Java 陷阱
1、 Java預設通路權限
在Java中,顯示的通路權限修飾符有private、protected、public,若在在定義類,屬性,方法時沒有顯示添加通路權限修飾符,則預設的為package,或稱friendly。
在Java中,若子類重寫父類的方法,要求子類的通路權限不能低于父類的通路權限,若父類為public,則子類隻能為public;若父類為protected,則子類可以為protected或public。
2、 類型提升
Java為強類型語言,當一個算術表達式中包含多個基本類型的值時,整個算術表達式的資料類型将發生自動提升,提升規則如下:
l byte、short、char提升為int;
l 這個表達式的計算結果類型提升為最高等級的操作數類型,如下

short age=8;
age=age+1;
上面的代碼将會報錯,由于age+1的結果為int類型(自動類型提升),age為short類型,是以将會報錯,需要強制類型轉換。
在Java中,long占8個位元組,short占4個位元組,為什麼long類型的可以轉換為float類型的?
long count=12;
float temp=count+1;
在Java中,long占8個位元組,short占4個位元組,為什麼long類型的可以轉換為float類型的?
因為long為整形,精确到個位,雖然占8個位元組,但表示數的範圍并沒有32位的float所表示的範圍大。
3、 switch
在Java中,switch子句的中變量可以為byte、short、char、int、Enum;但在JavaSE8中,switch中支援String類型。
4、 數組
在Java中,數組初始化有兩種方式,分為:
l 靜态初始化
l 動态初始化
靜态初始化是指在定義數組時,顯示指定數組元素,如下
int[] arr={1,2,3,4,5,6};
動态初始化如下:
int[] arr=new int[8];
for (inti = 0; i < arr.length; i++)
{
arr[i]=i;
}
在Java中數組為引用類型,是以不管數組元素是基本資料類型還是引用類型,都是在堆上配置設定記憶體空間的。對于基本資料類型的數組,若在建立數組時沒有初始化,則會以“零值”來初始化數組中的所有元素;而引用類型的元素預設為null,是以對于引用類型的數組元素還要單獨為每個數組元素指派,如下圖所示
定義數組(未初始化)
數組初始化
在Java中其實數組元素就是變量,如基本資料類型數組,數組元素就是基本資料類型的變量,對于引用類型的數組,數組元素為引用類型的變量。同樣,在Java中n維數組元素實質上n-1維數組的變量,如int[][] arrs;arrs中的每一個元素為int[]的變量。
5、 文法糖
文法糖是指在計算機語言中添加的某種文法,這種文法對于語言的功能并沒有影響,但是更友善程式員使用,是以文法糖隻是編譯器的行為。
在Java中常用的文法糖包括自動裝箱、拆箱,變長參數,泛型擦除等,虛拟機運作時不支援這些文法,它們會在編譯階段被還原為基礎文法結構。
5.1、裝箱、插箱
在Java中自動裝箱、拆箱文法會在編譯後轉換為對應的包裝和還原方法,如下
Integer age=10;
int temp=age;
編譯後的位元組碼如下:
bipush 10
invokestatic #16 //Integer.valueOf;
astore_1
aload_1
invokevirtual #22 // Integer.intValue
5.2、For each
在java中數組和容器類型元素可以使用foreach文法來循環周遊,則在編譯後還原為對應的疊代器實作,這也就是為何需要被循環周遊的類,需要實作Iterable接口的原因。
5.3、變長參數
在java中,可以在方法的最後面加上變長參數,其實質上,這些變長參數在編譯後會轉變為數組。
5.4、泛型類型擦除
Java中最有迷惑性的文法糖也許就是泛型了。在Java中,泛型隻會在源碼中存在,在編譯時,會将這些泛型資訊擦除,并在相應的位置添加強制類型轉換的代碼,基于類型擦除的實作泛型被稱為僞泛型。如下所示
public class Test
{
public void list(List<Integer>list)
{
return null;
}
public void list(List<String> list)
{
return null;
}
}
上面的代碼,重載了list方法,但編譯器會報錯,并不能編譯通過。原因在于方法參數類型一緻,List<Integer>list,List<String>list在編譯時類型擦除後,都為Listlist,二者方法參數相同,故不能夠重載。
6、 異常
在Java中,采用try-catch-finally語句塊進行異常捕捉和處理時,Java虛拟機向我們保證finally語句塊總會執行,事實真的如此嗎?
當我們在try、catch語句塊包含return語句時,finally語句塊還會執行嗎?當try-catch、finally語句都包含return語句時,傳回值到底是什麼?當try、catch語句塊包含System.exit(0)語句時,finally語句塊還會執行嗎?
6.1、return
情形1
public class Main
{
public static void main(String[] args)
{
System.out.println("retuen age="+getAge());
}
public static int getAge()
{
int age=10;
try
{
age=12;
returnage;
}
catch (Exceptione)
{
age=15;
}
finally
{
age=18;
System.out.println("finally...,age="+age);
}
returnage;
}
}
輸出結果如下:
finally...,age=18
retuen age=12
情形2
public class Main
{
public static void main(String[] args)
{
System.out.println("retuen age="+getAge());
}
public static int getAge()
{
int age=10;
try
{
age=12;
returnage;
}
catch (Exceptione)
{
age=15;
}
finally
{
age=18;
System.out.println("finally...,age="+age);
returnage;
}
}
}
輸出結果如下:
finally...,age=18
retuen age=18
當Java程式執行try塊,catch塊遇到了return語句,return語句并不會導緻該方法立即結束。而是去尋找該方法中是否包含了finally塊,如果沒有finally塊,方法終止,傳回相應的傳回值。如果有finally塊,系統立即開始執行finally塊,隻有當finally塊執行完畢後,系統才會再次跳回return語句處結束方法。如果finally塊中有return語句,則finally塊已經結束了方法,系統不會再跳回去執行try塊或catch塊中的任何代碼。
若是在try,catch中又抛出其他異常,也即遇到throw語句,則執行流程與return語句一緻。
總的來說,即使try塊或catch塊中有return(或throw)語句,finall語句塊也會被執行。若finally中有return語句,則執行finally塊中的return語句;若finally塊中沒有return語句,則傳回try塊或catch塊中的傳回值。
6.2、exit
public class Main
{
public static void main(String[] args)
{
System.out.println("retuen age="+getAge());
}
public static int getAge()
{
int age=10;
try
{
age=12;
System.exit(0);
}
catch (Exceptione)
{
age=15;
}
finally
{
age=18;
System.out.println("finally...,age="+age);
}
return age;
}
}
若在程式中遇到System.exit(0)語句将使虛拟機停止工作,finally語句并不能被執行。但是,當遇到System.exit(0)語句使虛拟機停止工作前,虛拟機會在退出前執行清理工作,會執行系統中注冊的所有關閉鈎子,如下
public class Main
{
public static void main(String[] args)
{
System.out.println("retuen age="+getAge());
}
public static int getAge()
{
int age=10;
//為系統注冊關閉鈎子
Runtime.getRuntime().addShutdownHook(newThread(new Runnable()
{
@Override
publicvoid run()
{
System.out.println("系統關閉鈎子被執行...");
}
}));
try
{
age=12;
System.exit(0);
}
catch (Exceptione)
{
age=15;
}
finally
{
age=18;
System.out.println("finally...,age="+age);
}
return age;
}
}
綜上所述,當系統顯示調用system.exit來退出程式時,若有可能未釋放的資源,需要注冊系統關閉鈎子來正确釋放資源。
6.3、正确關閉資源
如何在finally語句塊中關閉資源呢?
public void release()
{
ObjectInputStream in=null;
ObjectOutputStream out=null;
try
{
File file=new File("/Users/ssl/obj.data");
in=new ObjectInputStream(new FileInputStream(file));
out=new ObjectOutputStream(new FileOutputStream(file));
//...
}
catch (Exceptione)
{
}
finally
{
if (out!=null)
{
try
{
out.close();
}
catch (IOExceptione)
{
}
}
if (in!=null)
{
try
{
in.close();
}
catch (IOExceptione)
{
}
}
}
}
需要注意的是在關閉資源時,可能也會有異常,是以在關閉資源時也要顯示的捕獲異常,這樣做的好處是:若一個方法内打開多個實體資源,不能因為一個資源在關閉時抛出異常而影響其他資源的釋放。
6.4、繼承異常
在Java中規定,子類重寫父類的方法時,不能抛出比父類方法類型更多,範圍更大的異常,也即子類隻能抛出父類抛出異常的子類。
若子類實作多個接口,而且接口中方法聲明都相同,則子類實作方法時,隻能抛出多個接口抛出異常的交集,如下
//接口A
public interface InterfaceA
{
void sayHi()throws IOException;
}
//接口B
public interface InterfaceB
{
void sayHi()throws ClassNotFoundException;
}
//實作類
public class Implement implements InterfaceA,InterfaceB
{
@Override
public void sayHi()
{
System.out.println("hi...");
}
}
public class Main
{
public static void main(String[] args) throws IOException,ClassNotFoundException
{
Implement implement=new Implement();
InterfaceA interfaceA=implement;
InterfaceB interfaceB=implement;
interfaceA.sayHi();
interfaceB.sayHi();
}
}
7、 類與對象
在Java中,可以通過new關鍵字來建立某一個類的執行個體,那麼JVM通過那些資訊來建立類的執行個體,這些資訊存儲在什麼地方?為了描述類的資訊,當一個類被JVM加載後,就會在JVM中生成一個Class執行個體,以該執行個體作為操作類的入口,而且同一個JVM内一個類隻有一個Class執行個體,是以本質上類也是一個執行個體。
了解了類與對象的關系後,我們需要了解類變量(static修飾的變量是類變量,屬于類本身)與執行個體變量的初始化過程,以及構造函數的作用到底是什麼?
7.1、類構造函數
在.java檔案中,我們定義的構造函數嚴格來說是對象構造函數,隻有建立該類的執行個體時才會調用對象構造函數。那麼什麼是類構造函數呢?
我們知道,一個類對應一個Class對象。當一個類被JVM加載後,便會生成一個Class對象,其實類構造函數就是用來初始化Class對象的,那麼類構造函數是如何生成的,在.java 檔案中又沒有定義?
我們知道類變量有兩種初始化方式:
l 定義類變量時指定初始值;
l 在靜态代碼塊中,給靜态變量指派;
其實類構造函數就是就是根據上面兩種初始化類變量的代碼由JVM自動生成類構造函數,類構造函數中代碼的順序與.java檔案中初始化類變量的順序一緻,如下
//源檔案
public class Base
{
static
{
count=4;
}
static int count=3;
}
System.out.println(Base.count);
運作結果
3
此外,JVM保證在執行類構造函數前一定會先執行父類的類構造函數;而且,JVM會保證類構造函數執行過程中的線程加鎖和同步,這也就是在實作單例時直接為類變量指定值的原因,如下
//
public class Base
{
static
{
count=4;
System.out.println("base static....");
}
static int count=3;
}
//
public class Sub extends Base
{
static int age;
static
{
System.out.println("sub static.....");
}
}
//測試代碼
public static void main(String[] args)
{
System.out.println(Sub.age);
}
輸出結果:
base static....
sub static.....
在此,我們必須清楚不管是類構造器還是對象構造器作用都是執行初始化,在執行初始化操作之前,對象的記憶體已配置設定完畢,并置0值(數值類型為0,布爾值為false,引用類型為null)。其實Java和大多數語言一樣,都是采用的兩階段建造對象技術。在第一階段,配置設定記憶體空間,并置0;第二階段才執行使用者的初始化代碼,也即執行類構造函數或對象構造函數。如下
//源檔案
public class Base
{
int count=5;
}
//位元組碼
public class base.Base
{
int count;
public base.Base();
Code:
0: aload_0
1: invokespecial #10 //Method "<init>":()
4: aload_0
5: iconst_5
6: putfield #12 // Field count:I
9: return
}
上面的代碼中,雖然我們在定義count時,指定了初始值,但實際上初始化的代碼移動到了構造函數中。
7.2、對象構造函數
類構造函數是用來初始化Class執行個體的,而對象構造函數是用來初始化某一個類的執行個體。我們知道為執行個體屬性指定初始值有三種方式:
l 在定義執行個體屬性時,指派;
l 在非靜态代碼塊中,指派;
l 在構造函數中,指派;
其實,在定義執行個體屬性時指派或在非靜态代碼塊中指派這些操作最終都會被轉移到構造函數中,而且會被轉移到構造函數頭部,也即構造函數的指派操作晚于前兩種方式,而前兩種指派的先後與在.java源檔案中的順序一緻,如下
//源檔案
public class Cat
{
public Cat()
{
weight=2.6f;
}
{
weight=2.0f;
}
float weight=2.3f;
}
//位元組碼
public classbase.Cat
{
float weight;
public base.Cat();
Code:
0: aload_0
1: invokespecial #10 // Method "<init>":()
4: aload_0
5: fconst_2
6: putfield #12 // Field weight:F
9: aload_0
10: ldc #14 // float 2.3f
12: putfield #12 // Field weight:F
15: aload_0
16: ldc #15 // float 2.6f
18: putfield #12 // Field weight:F
21: return
}
總結,當執行個體化一個子類對象時,會執行子類的類構造函數,但JVM保證父類類構造函數先于子類類構造函數執行,是以先執行父類類構造函數,再執行子類類構造函數。之後再執行子類的構造函數,若子類構造函數中沒有顯式調用父類的構造函數,JVM會調用父類的預設構造函數,之後才會執行子類的構造函數。整體的順序為:父類類構造函數->子類類構造函數->父類構造函數->子類構造函數。
7.3、繼承屬性
子類繼承父類的屬性時,會在子類對象中儲存所有父類的屬性,包括私有的和公有的。若父類和子類中有相同的屬性,在子類對象中也會儲存兩份屬性。如下
//
public class Base
{
int count=5;
}
//
public class Sub extends Base
{
int count=10;
void showBase()
{
System.out.println("base count="+super.count);
}
void showSub()
{
System.out.println("sub count="+count);
}
}
//
public static void main(String[] args)
{
Sub sub=new Sub();
sub.showBase();
sub.showSub();
}
//輸出結果
base count=5
sub count=10
從上面的代碼中,可以看出子類對象确實儲存了父類對象的屬性,那麼子類是如何繼承父類的屬性呢,是簡單的把父類的屬性拷貝至子類中,還是其他方式?
public classbase.Base
{
int count;
public base.Base();
Code:
0: aload_0
1: invokespecial #10 //Method Object."<init>":()V
4: aload_0
5: iconst_5
6: putfield #12 // Field count:I
9: return
}
public classbase.Sub extends base.Base
{
int count;
public base.Sub();
Code:
0: aload_0
1: invokespecial #10 // Methodbase/Base."<init>":()V
4: aload_0
5: bipush 10
7: putfield #12 // Field count:I
10: return
}
從位元組碼檔案中,我們看出子類并沒有簡單的将父類中的屬性拷貝到子類中,而是各自屬性都在各自類中,隻不過在執行個體化子類時,會在子類對象配置設定的記憶體空間中包含所有父類的屬性。
7.4、繼承方法
繼承方法與繼承屬性有什麼差別嗎?
class Base
{
public void show()
{
System.out.println("show...");
}
}
public class Sub extends Base
{
}
//位元組碼
class base.Base
{
base.Base();
Code:
0: aload_0
1: invokespecial #8 // Method Object."<init>":()V
4: return
public void show();
Code:
0: getstatic #15 // Fieldjava/lang/System.out:Ljava/io/PrintStream;
3: ldc #21 // String show...
5:invokevirtual #23 //Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
public classbase.Sub extends base.Base
{
public base.Sub();
Code:
0: aload_0
1: invokespecial #8 //Method base/Base."<init>":()V
4: return
public void show();
Code:
0: aload_0
1: invokespecial#15 // Method base/Base.show:()V
4: return
}
從上面的位元組碼可以看出,編譯器會将子類中的方法轉移到子類,若子類中有一樣的方法,則父類中的方法不會轉移到子類中。
注意,父類不使用public修飾,而子類使用public,才可以看到編譯器将父類中的方法轉移到子類中。
由于繼承方法時,對于public方法子類可以覆寫父類中的方法,對于其他private,protected或package方法,子類并不能覆寫;若子類中有同樣簽名的方法,不過是子類中的方法而已。而繼承屬性時,父類中的所有屬性都會在子類中存儲,是以這就導緻調用方法和通路屬性時的行為并不一緻。總結來說通路屬性時,依據的是編譯期對象的類型(字面類型);而調用方法時,依據的是對象的實際類型,如下
//基類
public class Base
{
private int count=10;
public Base()
{
count=2;
show();
}
public void show()
{
System.out.println("count="+count);
}
}
//子類
public class Sub extends Base
{
private int count=20;
public void show()
{
System.out.println("count="+count);
}
}
//測試
public static void main(String[] args)
{
new Sub();
}
//輸出結果
count=0
對于上面的代碼,是不是對輸出結果,心存疑慮呢?我們先來看看上面代碼的一個執行過程。
new Sub()将會調用Sub的預設構造函數來初始化執行個體,但Sub繼承自Base,是以JVM先執行Base的預設構造函數,
public Base()
{
count=2;
show();
}
count=2,實質是this.count=2,那麼此處的this到底是Base還是Sub,實質上在運作時this指向Sub執行個體,但this.count=2是處于Base的構造函數内,是以在編譯期this為Base類型,那麼count=2,實際上為Base的執行個體count屬性指派為2,是以父類的count值為2。
show(),實質上this.show(),由于方法調用是運作時實際對象類型的方法,是以這裡會執行Sub執行個體的show方法,Sub執行個體的show方法如下
public void show()
{
System.out.println("count="+count);
}
這裡輸出的是Sub執行個體的count屬性,而此時count屬性值隻是在配置設定記憶體時的0值,Sub的構造函數内的代碼還未執行,是以程式将輸出0值。當show方法執行完畢後,才執行Sub的構造函數内的代碼。若想弄明白具體的執行過程,可以在Base和Sub的構造函數中添加輸出語句,如下
public class Base
{
private int count=10;
public Base()
{
count=2;
System.out.println("base constructor count="+count);
show();
}
public void show()
{
System.out.println("count="+count);
}
}
public class Sub extends Base
{
private int count=20;
public Sub()
{
System.out.println("sub constructor count="+count);
}
public void show()
{
System.out.println("count="+count);
}
}
//測試
public static void main(String[] args)
{
new Sub();
}
//輸出結果
base constructor count=2
count=0
sub constructor count=20
7.5、編譯期綁定和運作時綁定
在Java中,多态行為是依靠運作時綁定才實作的,也即在運作時才确定對象的實際類型。但在Java中是不是所有的類型都是在運作時才确定呢?
答案是否定的,在Java中存在一些“編譯期可知,運作期不可變”的類型,是以對于這些類型将在編譯期完成綁定(根據根據聲明的類型來綁定),具體有以下幾種情況:
l 靜态方法方法
l 私有方法
l 重載方法,在Java中重載的方法是在編譯期間綁定的;而重寫是多态行為,是在運作期綁定的;
l 執行個體屬性,由于子類也儲存了父類中的所有屬性,是以在通路屬性時,依據的是編譯期類型,如下
public class Base
{
int count=10;
}
public class Sub extends Base
{
int count=20;
}
//測試
public static void main(String[] args)
{
Sub sub=new Sub();
Base base=sub;
System.out.println(sub.count);
System.out.println(base.count);
}
//輸出結果
20
10
7.6、final
在Java中final可以修飾類、方法、屬性,意味為不可變,具體如下:
l 修飾類時,該類不能被繼承;
l 修飾方法時,該方法不能被重寫;
l 修飾屬性時,為不可變屬性,對于基本類型的資料,這些值不能改變;對于引用類型,引用不能再指定其他的執行個體,而指向的執行個體本身是可以改變的;
對于final修飾的屬性,并且指定了初始值,如final int count=5;就可以在編譯期确定屬性的類型,那麼這個final屬性已不再是變量,而是相當于一個直接常量。
8、 内部類
在Java中,内部類分為非靜态内部類、靜态内部類和局部内部類等。
8.1、非靜态内部類
在Java中,非靜态内部類,可以通路外部類的所有的屬性和方法,那麼為什麼内部類可以通路外部類的屬性和方法呢?
内部類持有外部類對象執行個體的引用,如下所示
public class Person
{
class Address
{
private Stringinfo;
public Address()
{
}
public Address(Stringinfo)
{
}
}
}
内部類對應的位元組碼如下
classbase.Person$Address
{
final base.Person this$0;
public base.Person$Address(base.Person);
Code:
0: aload_0
1: aload_1
2: putfield #12 // Field this$0:Lbase/Person;
5: aload_0
6: invokespecial #14 //Methodjava/lang/Object."<init>":()V
9: return
public base.Person$Address(base.Person, java.lang.String);
Code:
0: aload_0
1: aload_1
2: putfield #12 // Field this$0:Lbase/Person;
5: aload_0
6: invokespecial #14 // Methodjava/lang/Object."<init>":()V
9: return
}
從位元組碼可以看出非靜态内部類中持有外部類的引用,且非靜态内部類的構造函數在編譯之後會增加外部類引用的參數,以便初始化該内部類。是以,非靜态内部類必須依賴外部類的執行個體而存在,是以導緻非靜态内部類不能擁有靜态屬性或方法。
8.2、靜态内部類
被static修飾的成員,如屬性,方法,塊代碼等屬于類本身,而不屬于執行個體,那麼對于static修飾的内部類,也是如此,靜态内部類屬于外部類,而不屬于外部類的執行個體。是以,對于靜态内部類不能通路外部類的執行個體屬性或方法,其實對于靜态内部類而言,外部類隻不過相當于一個包而已。
8.3、局部内部類
對于在方法或其他代碼塊中的内部類,稱為局部内部類。在局部内部類中,若要通路外部變量,必須要求外部變量為final的。原因如下,對于普通變量而言,作用域在方法體内,方法執行完畢,局部變量也就不存在了;但是對于内部類而言,可能會産生隐式的閉包,閉包将使得擴大局部變量的作用域,使其脫離方法體後,繼續存在。如下
public static void run()
{
final int count=10;
new Thread(new Runnable()
{
class Count
{
void cutdown()
{
inti=count;
while (i>0)
{
System.out.println("cutdowni="+i);
i--;
}
}
}
@Override
public void run()
{
new Count().cutdown();
}
}).start();
}
由于count局部變量,在局部内部類中仍要被通路,擴大了count變量的作用域。由于可能産生隐式閉包,擴大局部變量的作用域,為了安全起見,Java編譯器要求被内部類通路的變量必須使用final修飾符修飾。
9、 内置鎖
Java内置鎖分為對象鎖和類鎖,類鎖和該類對象的鎖并不是一把鎖,是不能用于互斥操作的,在Java中若想達到互斥的效果,對臨界區必須使用同一把鎖進行加鎖。如下,執行個體方法和類方法并并不能互斥。
public class Main
{
public static void main(String[] args)
{
new Thread(new Runnable()
{
@Override
public void run()
{
Test.classMethod();
}
}).start();
new Thread(new Runnable()
{
@Override
public void run()
{
new Test().instanceMethod();
}
}).start();
}
static class Test
{
public static synchronized void classMethod()
{
for (inti = 0; i < 10;i++)
{
System.out.println("class method of loop "+i);
}
}
public synchronized void instanceMethod()
{
for (inti = 0; i < 10;i++)
{
System.out.println("instance method of loop "+i);
}
}
}
}
輸出結果:
class method of loop 0
class method of loop 1
instance method of loop 0
class method of loop 2
class method of loop 3
instance method of loop 1
class method of loop 4
instance method of loop 2
class method of loop 5
instance method of loop 3
instance method of loop 4
class method of loop 6
instance method of loop 5
instance method of loop 6
instance method of loop 7
instance method of loop 8
instance method of loop 9
class method of loop 7
class method of loop 8
class method of loop 9