6 類與對象
Java是面向對象的程序設計語言,類是面向對象的重要內容,可以把類當成一種自定義類型,可以使用類來定義變量,這種類型的變量統稱爲引用變量。所有類都是引用變量。
6.1 定義類
面向對象的程序設計過程中有兩個重要概念:類(class)和對象(object,也被稱爲實例,instance),其中類是某一批對象的抽象,可以把類理解成某種概念;對象纔是一個具體存在的實體。
6.1.1 定義類的簡單語法格式如下:
[修飾符] class 類名
{
零到多個構造器定義...
零到多個成員變量...
零到多個方法...
對定義類語法格式的詳細說明如下:
- 修飾符可以是public、final、abstract,或者完全省略三個修飾符,類名只要是一個合法的標識符即可。
- 定一個類定義而言,可以包括三種最常見的成員:構造器,成員變量和方法。
- static修飾的成員不能訪問沒有static修飾的成員
- 構造器是一個類創造對象的根本途徑,如果一個類沒有構造器,這個類通常無法創建實例。因此,Java語言提供了一個功能:如果程序員沒有一個類編寫的構造器,則系統會爲該類提供一個默認的構造器,一旦程序員爲一個類提供了構造器,系統將不再爲該類提供構造器了。
6.1.2 定義成員變量的簡單語法格式如下:
[修飾符] 類型 成員變量名 [=默認值];
對定義成員變量語法格式的詳細說明如下:
- 修飾符:修飾符可以省略,也可以是public、protected、private、static、final,其中public、protected、private三個最多隻能出現其中之一,可以與static、final組合起來修飾成員變量。
- 類型:類型可以是Java語言中允許的任何數據類型,包括基本類型和現在介紹的引用類型。
- 成員變量名:只要合法即可,但是還是不要亂起名了。
- 默認值:定義一個成員變量還可以指定一個可選的默認值。
6.1.3 定義方法的語法格式如下:
[修飾符] 方法返回值類型 方法名(形參列表)
{
//由零條到多條可執行性語句組成的方法體
}
對定義方法語法格式的詳細說明如下:
- 修飾符:修飾符可以省略,也可以是public、protected、static、final、abstract,其中public、protected、private三個最多隻能出現一個;final和abstract最多隻能出現其中一個,他們可以以static組合起來修飾方法。
- 方法返回值類型:返回值類型可以是Java語言允許的任何數據類型,包括基本類型和引用類型;如果聲明瞭方法返回類型,則方法體內必須有一個有效的return語句。
- 方法名:方法名的命名規則與成員變量的命名規則相同
- 形參列表:形參列表用於定義該方法可以接受的參數。
方法體裏多條執行語句之間有嚴格的執行順序,排在方法體面前的語句總是先執行,排在方法後的語句總是慢執行。
static 是一個特殊的關鍵字,它可用於修飾方法、成員變量等成員。static修飾的成員表明它屬於這個類本身,而不屬於該類的單個實例,因爲通常把static修飾的成員變量和方法也稱爲類變量、類方法。不適用static修飾的普通方法、成員變量則屬於該類的單個實例,而不屬於該類。通常把不使用static修飾的成員變量和方法也稱爲實例變量、實例方法。
由於static的英文直譯就是靜態的意思,因此有時也把static修飾的成員變量和方法稱爲靜態變量和靜態方法,把不使用static修飾的成員變量和方法稱爲非靜態變量和非靜態方法。靜態成員不能直接訪問非靜態成員。
6.1.3 定義構造器的語法格式如下:
[修飾符] 構造器名(形參列表)
{
// 由零條到多條可執行語句組成的構造器執行體
}
對定義構造器語法格式的詳細說明如下:
- 修飾符:修飾符可以省略,也可以是public、protected、private其中之一
- 構造器名:構造器名必須和類名相同
- 形參列表:和定義方法形參相同
值得指出,構造器既不能定義返回值類型,也不能使用void聲明構造器沒有返回值。如果爲構造器定義了返回值類型,或使用void聲明構造器沒有返回值,編譯時不會出錯,但Java會把這個所謂的構造器當成方法來處理。
6.2 對象的產生和使用
6.2.1創造一個對象
在這裏寫一個Person類:
public class Person
{
//下面定義兩個成員變量
public String name;
public int age;
//下面定義了一個say方法
public void say(String content)
{
System.out.println(content);
}
}
6.2.2 創建一個實例
創建對象的根本途徑是構造器,通過new關鍵字來調用某個類的構造器即可創建這個類的實例
//使用Person類定義一個Person類型的變量
Person p;
//通過new關鍵字調用Person類的構造器,返回一個Person實例
//將該Person實例賦給p變量
p = new Person();
上面代碼也可簡寫成如下形式:
//定義p變量的同時併爲p變量賦值
Person p = new Person();
6.2.3 調用實例變量和方法
static 修飾的方法和成員變量,即可通過類來調用,也可以通過實例來調用;沒有使用static修飾的普通方法和成員比變量,只可通過實例來調用。
//訪問p的name實例變量,直接爲該變量賦值
p.name = "李剛";
//調用p的say()方法,聲明say()方法時定義了個形參
p.say("Java語言很簡單,學習很容易!");
//直接輸出p的name實例變量,將輸出 李剛
System.out.println(p.name);
6.2.4 對象的this引用
Java提供了一個this關鍵字,this關鍵字總是指向調用該方法的對象。根據this出現位置的不同,this作爲對象的默認引用的兩種情形:
- 構造器中引用該構造器正在初始化的對象
- 在方法中引用調用該方法的對象
public class Dog
{
//定義一個jump()方法
public void jump()
{
System.out.println("正在執行jump方法");
}
public void run()
{
this.jump();
System.out.println("正在執行run方法");
}
}
對於static修飾的方法而言,則可以使用類來直接調用該方法,如果在static修飾的方法中使用this關鍵字,則這個關鍵字就無法指向合適的對象。所以,static修飾的方法中不能使用this引用,由於static修飾的方法不能使用this引用,所以static修飾的方法不能訪問不適用static修飾的普通成員:靜態成員不能直接訪問非靜態成員
下面程序演示了靜態方法直接訪問非靜態方法時引發錯誤。
public class StaticAccessNonStatic
{
public void info()
{
System.out.println("簡單的info方法");
}
public static void main(String[] args)
{
// 因爲main()方法是靜態方法,而info()是非靜態方法
// 調用main的方法的是該類本身,而不是該類的實例
//因此省略的this無法指向有效的對象
info();
}
}
會出現下面這樣的錯誤:
無法從靜態上下文中引用非靜態方法 info()
注意:Java中static修飾的成員屬於類本身,而不屬於該類的實例,既然static修飾的成員完全不屬於該類的實例,那麼就不應該允許使用實例去調用static修飾的成員變量和方法!記住:Java編程時不要使用對象去調用static修飾的成員變量、方法,而是應該使用類去調用static修飾的成員變量、方法!
除此以外,this引用也可以用於構造器中作爲默認引用,由於構造器是直接使用new關鍵字來調用,而不是使用對象來調用的,所以在this在構造器中代表該構造器正在初始化的對象。
public class ThisInConstructor
{
//定義一個名爲foo的成員變量
public int foo;
public ThisInConstructor()
{
//在構造器裏定義一個foo變量
int foo = 0;
/// 使用 this 代表該構造器正在初始化的對象
// 下面的代碼將會把該構造器正在初始化的對象的 foo 成員變量6
public static void main(String[] args)
{
//所有使用 ThislnConstructor 創建的對象的 foo 成員變量
// 都將被設爲 ,所以下面代碼將輸出6
System.out.println(new ThisInConstructor().foo);
}
}
6.3 方法詳解
如果需要定義方法,則只能在類體內定義,不能獨立定義一個方法,如果這個方法使用了static修飾,則這個方法屬於這個類,否則這個方法屬於這個類的實例。
Java語言裏的方法的所屬性主要體現在如下方面:
- 方法不能獨立定義,方法只能在類體裏定義
- 從邏輯意義上來看,方法要麼屬於該類本身,要麼屬於該類的一個對象
- 永遠不能獨立執行方法,執行方法必須使用類或對象作爲調用者
使用 static 修飾的方法屬於這個類本身,使用 static 修飾的方法既可以使用類作爲調用者來調用,可以使用對象作爲調用者來調用。但值得指出的是,因爲使用 statlc 修飾的方法還是屬於這個類的,因此使用該類的任何對象來調用這個方法時將會得到相同的執行結果,這是由於底層依然是使用這些實例所屬的類作爲調用者。
6.3.1 方法的參數傳遞機制
&esmp; Java裏方法的參數傳遞方式只有一種:值傳遞。所謂值傳遞,就是將實際參數值的副本傳入方法內,而參數本身不會受到任何影響。
class DataWrap
{
int a;
int b;
{
public class ReferenceTranferTest
{
public static void swap(DataWrap dw)
{
//下面三行代碼實現dw的a、b兩個成員變量的值交換
// 定義一個臨時變量來保存dw對象的a成員變量的值
int tmp = dw.a;
dw.a = dw.b;
dw.b = tmp;
System.out.println("Swap方法裏,a成員變量的值是"+dw.a + ":b成員變量的值是"+dw.b);
}
public static void main(String[] args)
{
DataWrap dw = new DataWrap();
dw.a = 6;
dw.b = 9;
swap(dw);
System.out.println("交換結束後,a成員變量的值是" + dw .a + " ; 成員變量的值是 + dw .b) ;
}
}
程序從main()方法開始執行,main()方法開始創建了一個DataWrap對象,並定義一個dw引用變量來指向DataWrap對象,這是一個與基本類型不同的地方。
6.3.2 形參個數可變的方法
Java允許定義形參個數可變的參數,從而允許爲方法指定數量不確定的形參。如果在定義方法時,在最後一個形參的類型後增加三點(…),則表明該形參可以接受多個參數值,多個參數值被當成數組傳入。
public class Varargs
{
//定義了形參個數可變的方法
public static void test(int a, String... boos)
{
//books 被當數組出路
for(String tmp : books)
{
System.out.peintln(tmp);
}
// 輸出整數變量a的值
System.out.println(a);
}
public static void main(String[] args)
{
// 調用test方法
test(5, "哈哈哈","123h");
}
}
6.3.3 方法重載
Java 允許同一個類裏定義多個同名方法,只要形參列表不同就行。如果同一個類中包含了兩個或兩個以上方法的方法名相同,但形參列表,則被稱爲方法重載。
從上面介紹可以看出,在Java程序中確定一個方法需要三個要素:
- 調用者,也就是方法的所屬者,既可以是類,也可以是對象
- 方法名,方法的標識
- 形參列表,當調用方法時,系統將會根據傳入的實參列表匹配。
public class Overload
{
//下面定義了兩個test()方法
public void test()
{
System.out.println("無參數");
}
public void test(String msg)
{
System.out.println("重載的test方法" + msg);
}
public static void main(String[] args)
{
OverLoad ol = new Overload();
ol.test();
ol.test("heelo");
}
}
6.4 成員變量和局部變量
變量可以分爲兩大類:成員變量和局部變量
6.4.1 成員變量和局部變量是什麼
成員變量指的是在類裏定義的變量,也就是前面所介紹的field:局部變量指的是在方法裏方法裏定義的變量。
成員變量被分爲類變量和實例變量兩種,定義成員變量時沒有static修飾的是就是實例變量,有static修飾的就是類變量。
6.5 隱藏和封裝
前面程序中經常出現通過某個對象的直接訪問其成員變量的情形,比如將某個Person的age成員變量直接設爲1000,顯然不合理。
6.5.1 理解封裝
封裝(Encapsulation)是面向對象的三大特性之一(另外兩個是繼承和多態),它指的是將對象的狀態信息隱藏在對象內部,不允許外部程序直接訪問對象內部信息,而是通過該類所提供的方法來實現對內部信息的操作和訪問。
對一個類進行封裝,可以實現下面目的:
- 隱藏類的實現細節
- 讓使用者只能通過事先預定的方法來訪問數據,從而可以在該方法里加入控制邏輯,限制對成員變量的不合理訪問
- 可進行數據檢查,從而有利於保證對象信息的完整性
- 便於修改,提高代碼的可維護性
6.5.1 使用訪問控制符
Java提供三個訪問控制符:private、protected和public,分別代表了3個訪問控制級別,另外還有一個不加任何訪問控制符的訪問控制級別。詳細介紹如下:
- private(當前類訪問權限):如果類裏的一個成員(包括成員變量、方法和構造器等)使用private訪問控制符來修飾,則這個成員只能在當前類的內部被訪問。這個訪問控制符用於修飾成員變量最合適,使用它來修飾成員變量就可以把成員變量隱藏在該類得內部。
- default(包訪問權限):如果類裏的一個成員(包括成員變量、方法和構造器等)或者一個個外部類不使用任何訪問控制符修飾,就稱它使包訪問權限的,default訪問控制的成員或外部類可以被相同包下的其他類訪問。
- protected(子類訪問權限):如果一個成員(包括成員變量、方法和構造器等)使用protected訪問控制符修飾,那麼這個成員既可以被同一個包中的其他類訪問,也可以被不同包中的子類訪問。
- public(公共訪問權限):這個是一個最寬鬆的訪問控制級別,如果一個成員或者一個外部類使用public訪問控制符修飾,那麼這個成員或外部類就可以被所有類訪問,不管訪問類和被訪問類是否處於同一個包中,是否具有父子繼承關係。
對於外部類而言,它也可以使用訪問控制符修飾,但外部類只能有兩種訪問控制級別:public和默認,外部類不能使用private喝protected修飾,因爲外部類沒有處於任何類的內部,也就沒有其他所在類的內部、所在類的子類兩個範圍,所以private和protected訪問控制符對外部類沒有意義。
外部類可以使用 public 包訪問控制權限 ,使用 public 修飾的外部類可以被所有類使用,如聲明變量、創建實例 不使用任何訪問控制符修飾的外部類只能被同 個包中的其他類使用。
public class Person
{
//使用private修飾成員變量,將這些成員變量隱藏起來
private String name;
private int age;
//提供方法來操作name成員變量
public void setName(String name)
{
//執行合理性校驗,要求用戶名必須在2~6之間
if (name.length() > 6 || name.length() < 2}
{
System.out.println("您設置的人名不符合要求");
return;
}
else
{
this.name = name;
}
}
public String getName()
{
return this.name
}
//提供方法來操作age成員變量
public void setAge(int age)
{
//執行合理性校驗,要求用戶年齡必須在0~100之間
if(age > 100 || age < 0)
{
System.out.println("您設置的年齡不合法");
return;
}
else
{
this.age = age;
}
public int getAge()
{
return this.age
}
}
注意:Java類裏實例變量的setter和getter方法有非常重要的意義。例如,某個類裏包含了一個名爲abc的實例變量,則其對應的setter和getter方法名應爲setAbc() 和 getAbc()(將原實例變量名的首字母大寫,並在前面分別增加 set get 動坷,就變成 setter,getter方法名)。
關於訪問控制符的使用,存在如下幾條基本原則
- 類裏絕大部分成員變量都應該使用private修飾,只有一些static修飾的、類似全局變量的成員變量,纔可能考慮使用public修飾,除此之外,有些方法只用於輔助實現該類的其他方法,這些方法被稱爲工具方法,工具方法也應該使用private修飾
- 如果某個類主要用做其他類的父類,該類裏包含的大部分方法可能希望被其子類重寫,而不希望被外界調用,則應該使用protected修飾這些方法。
- 希望暴露出來給其他類自由調用的方法應該使用public修飾。因此類的構造器通過使用public修飾,從而允許其他地方創建該類的實例,因爲外部類通常希望都能夠被其他類自由使用,所以大部分外部類都使用public修飾。
6.6 package、import和import static
前面提到包範圍,先來回憶一個場景:在我們漫長的求學、工作生涯中可曾遇到過與自己同名的同學或同事?因爲筆者姓名的緣故,筆者經常會遭遇此類事情。如果同一個班級裏出現兩個叫“李剛”的同學,那老師怎麼處理呢?老師通常會在我們的名字前增加一個限定,例如大李剛、小李剛以示區分。
Java允許將一組功能相關的類放在同一個package下,從而組成邏輯上的類庫單元。如果希望把一個類放在指定的包結構下,應該在Java源程序的第一個非註釋行放置如下格式的代碼:
package packageName;
一旦在Java源文件中使用了這個package語句,就意味着源文件裏定義的所有類都屬於這個包。位於包中的每個類的完整類名都應該是包名和類名的組合,如果其他人需要使用該包下的類,也應該使用包名加類名的組合。
package lee;
public class Hello
{
public static void main(String[] args)
{
System.out.println("Hello World");
}
}
在該目錄下運行:
javac -d . Hello.java
這時候不會單單出現一個 .java文件,還會出現lee文件夾,Hello.java文件會在lee目錄中。這時候需要執行文件即
java lee.Hello
上面定義的的位於lee包下的Hello.java及生成的Hello.class文件,建議以下圖形式存放:
爲了簡化編程,Java引入了import關鍵字,import可以向某個Java文件中導入指定包層次下某個類或全部類,import語句應該出現在package語句之後、類定義之前。一個Java源文件只能包含一個package語句,但是可以有多個import語句。使用import語句導入單個類的用法如下:
import package.subpackage...ClassName;
上面語句用於直接導入指定Java類,使用下面代碼:
import lee.sub.Apple;
使用import語句導入指定包下全部類的用法如下:
import package.subpackage...*;
上面import語句中的星號(*)只能代表類,不能代表包。因此使用import lee.*;語句時,它表明導入lee包下的所有類,即Hello類和Hello Test類,而lee包下的sub子包並不導入。
**注意:**Java默認爲所有源文件導入java.lang包下的所有類,因此前面在Java程序中使用String、System類時都無須類時都無須使用import語句來導入這些類,但對於前面介紹數組時提到的Arrays類,其位於java.util包下,則必須使用import語句來導入該類。
現在可以總結出Java源文件的大體結構定義:
package 語句 //0個或者1個,必須放在文件開始
import | import static 語句 //0或者多個
public classDefinition | interfaceDefinition | enumDefinition //0個或者一個public類,接口,枚舉定義
classDefinition | interfaceDefintion | enumDefinition //0個或多個普通類,接口或枚舉定義
6.6 深入構造器
構造器是一個特殊的方法,這個特殊的方法用於創建實例時執行初始化。構造器是創建對象的重要途徑。
6.6.1 使用構造器執行初始化
&esmp; 構造器最大的用出就是在創建對象時執行初始化。當創建一個對象時,系統爲這個對象的實例變量進行默認初始化,這種默認的初始化把所有基本類型的實例變量設爲0或false和null。
public class ConstructorTest
{
public String name;
public int count;
//自定義的構造器,該構造器包含兩個參數
public ConstructorTest(String name, int count)
{
this.name = name;
this.count = count;
}
public static void main(String[] args)
{
ConstructorTest tc = new ConstructorTest("ahha",900);
System.out.println(tc.name);
System.out.println(tc.count);
}
}
感覺有點像python中的__init__函數。
6.6.2 構造器重載
同一個類裏具有多個構造器,多個構造器的形參列表不同,即被稱爲構造器重載。構造器允許Java類裏包含多個初始化邏輯,從而允許使用不同的構造器來初始化Java對象。
public class ConstructorOverload
{
public String name;
public int count;
//無參數構造器
public ConstructorOverload(){}
//自定義的構造器,該構造器包含兩個參數
public ConstructorOverload(String name, int count)
{
this.name = name;
this.count = count;
}
public static void main(String[] args)
{
ConstructorOverload tc01 = new ConstructorOverload();
ConstructorOverload tc02 = new ConstructorOverload("ahha",900);
System.out.println(tc01.name + tc01.count);
System.out.println(tc02.name + tc02.count);
}
}
還有一種就是在構造器中調用另一個構造器的例子:
public class Apple
{
public String name;
public String color;
public double weight;
public Apple(){}
public Apple(String name, String color)
{
this.name = name;
this.color = color;
}
public Apple(String name, String color, double weight)
{
//通過 this 調用另 個重載的構造器的初始化代碼
this(name, color);
this.weigth = weight;
}
}
6.7 類的繼承
繼承是面向對象的三大特性之一,也是實現軟件複用的重要手段。Java繼承具有單繼承的特點,每個子類只有一個直接父類。
6.7.1 繼承的特點
Java的繼承通過extends來實現。如下:
修飾符 class SubClass extends SuperClass
{
//類定義部分
}
這個比較簡單,不寫了。
6.7.2 重寫父類的方法
子類擴展了父類,子類是一個特殊的父類。大部分時候。大部分時候,子類總是以父類爲基礎,額外增加新的成員變量和方法。但是也有需要子類重寫父類方法的情況:
public class Bird
{
public void fly()
{
System.out.println("我在飛...");
}
}
public class Ostrich extends Bird
{
//重寫Bird類的fly()方法
public void fly()
{
System.out.println("我在跑....");
}
public static void main(String[] args)
{
Ostrich os = new Ostrich();
os.fly();
}
}
這種子類包含父類同名的方法的現象稱爲方法的重寫,也稱爲方法的覆蓋。
方法的重寫需要遵守“兩同兩小一大”規則,“兩同”即方法名相同、形參列表相同;“兩小”指的是子類方法的返回值類型應該父類方法返回值類型更小或相等,子類方法聲明拋出的異常類應該比父類方法聲明拋出的異常類更小或相等;“一大”指的是子類方法的訪問權限比父類方法權限更大或者相等。
6.7.3 super限定
如果需要在子類方法中調用父類被覆蓋的實例方法,則可以使用super限定來調用父類被覆蓋的實力方法。
public void callOverrideMethod()
{
super.fly();
}
super是Java的一個關鍵字,super用於限定該對象調用它從父類繼承得到的實例變量或方法。正如this不能出現在static修飾的方法中一樣,super也不出現在static修飾的方法中。static修飾的方法是屬於類的,該方法的調用者可能是一個類,而不是對象。
如果子類裏沒有包含和父類同名的成員變 ,那麼在子類實例方法中訪問該成員變 時,則無須顯式使用 super 或父類名作爲調用者。如果在某個方法中訪問名爲 的成員變 ,但沒有顯式指定調用者,則系統查找a的順序爲:
- 查找該方法中是否有名爲a的局部變量
- 查找當前類中是否包含名爲a的成員變量
- 查找a的直接父類中是否包含名爲a的成員變量,依次上溯a的所有父類。
6.7.4 調用父類構造器
子類不會獲得父類的構造器,但子類構造器裏可以調用父類構造器的初始 代碼,類似於前面所介紹的一個構造器調用另一個重載的構造器。
個構造器中調用另一個重載的構造器使用 this 調用來完成,在子類構造器中調用父類構造器使super 調用來完成。
class Base
{
public double size;
public String name;
public Base(double size, String name)
{
this.size = size;
this.name = name;
}
}
public class Sub extends Base
{
public String color;
public Sub(double size,String name, String color)
{
super(size, name);
this.color = color;
}
public static void main(String[] args)
{
Sub s = new Sub(5.6, "ahda", "dad");
System.out.println(s.size + "--" + s.name + "--" + s.color);
}
}
super調用和this調用也很像,區別在於super調用的是父類的構造器,而this調用的是同一個類中重載的構造器。因此,使用super調用父類構造器也必須出現在子類構造器執行體的第一行,所以this調用和super調用不會同時出現。
class Creature
{
public Creature()
{
System.out.println("Creature無參數的構造器");
}
}
class Animal extends Creature{
public Animal(String name)
{
System.out.println("Aniaml自帶一個參數的構造器:" +name);
}
public Animal(String name, int age)
{
this(name);
System.out.println("Animal自帶兩個參數的構造器:" + name + age);
}
}
public class Wolf extends Animal
{
public Wolf()
{
super("狼", 3);
System.out.println("Wolf無參數的構造器");
}
public static void main(String[] args)
{
new Wolf();
}
}
運行結果:
Creature無參數的構造器
Aniaml自帶一個參數的構造器:狼
Animal自帶兩個參數的構造器:狼3
Wolf無參數的構造器
看得出來,構造器都是從最原始往下開始,然後如果有this調用同類的構造器,則依次執行。
6.8 多態
Java引用變量有兩個類型:一個是編譯時類型,一個運行時類型。編譯時類型由聲明該變量時使類型決定,運行時類型由實際賦給變量的對象決定。如果編譯時類型時運行時類型不一致,就可能出現所謂的多態。
class BaseClass
{
public int book = 6;
public void base()
{
System.out.println("父類的普通方法");
}
public void test()
{
System.out.println("父類的被覆蓋的方法");
}
}
public class SubClass extends BaseClass
{
public String book = "hahah";
public void test()
{
System.out.println("子類的覆蓋父類的方法");
}
public void sub()
{
System.out.println("子類的普通方法");
}
public static void main(String[] args)
{
BaseClass bc = new BaseClass();
//輸出6
System.out.println(bc.book);
//下面兩次調用將執行BaseClass的方法
bc.base();
bc.test();
//下面編譯時類型和運行時類型完全一樣,因此不存在多態
SubClass sc = new SubClass();
System.out.println(sc.book);
sc.base();
sc.test();
//下面編譯時類型和運行時類型不一樣,多態發生
BaseClass ploymophicBc = new SubClass();
System.out.println(ploymophicBc.book);
ploymophicBc.base();
ploymophicBc.test();
}
}
當把一個子類對象直接賦給父類引用變量時,例如上面的BaseClass ploymophicBc = new SubClass(); 這個ploymophicBc引用變量的編譯時類型爲BaseClass,而運行時類型時SubClass,當運行時調用該引用變量的方法時,其方法行爲總是表現出子類方法的行爲特徵,而不是父類方法的行爲特徵,這就可能出現:相同類型的變量、調用同一個方法時呈現出多種不同的行爲特徵,這就是多態。
上面的main()方法中如果使用ploymophicBc.sub();這行代碼會在編譯時引發錯誤。雖然ploymophicBc引用變量實際上確實包含sub()方法,但編譯類型爲BaseClass,因此無法調用sub()方法。
與方法不同的是,對象的實例變量則不具備多態性,比如上面的ploymophicBc引用變量,程序中輸出它的book實例變量時,並不是輸出SubClass類裏定義的實例變量,而是輸出BaseClass類的實例變量。
引用變量在編譯階段只能調用其編譯時類型所具有的方法,但運行時執行它運行時類型具有的方法,因此,編寫Java代碼的時候,引用變量只能調用聲明該變量時所用類包含的方法。通過引用變量來訪問其包含的實例變量時,系統總是試圖訪問它編譯時類型所定義的成員變量,而不是它運行時類型所定義的成員變量。
6.8.1 引用變量的強制類型轉換
編寫Java程序時,引用變量只能調用它編譯時類型的方法,而不能調用它運行時類型的方法,即使它實際所引用的對象確實包含該方法。如果需要讓這個引用變量調用它運行時類型的方法,則必須把它強制類型轉換成運行時類型,強制類型轉換需要藉助於類型轉換運算符。
類型轉換運算符是小括號,類型轉換運算符的用法是:(type)variable,這種用法可以將variable變量轉換成一個type類型的變量。這種強制類型轉換需要注意:
- 基本類型之間轉換只能在數值類型之間進行,這裏所說的數值類型包括整數型,字符型和浮點型。但數值類型和布爾類型之間不能進行類型轉換。
- 引用類型之間的轉換只能在具有繼承關係的兩個類型之間進行,如果是兩個沒有任何繼承關係的類型,是無法進行類型轉換的。如果試圖把一個父類實例轉換爲子類類型,則這個對象必須實際上是子類實例才行。
public class ConversionTest
{
public static void main(String[] args)
{
double d = 13.4;
long l = (long)d;
System.out.println(l);
int in = 5;
//試圖把一個數值類型的變量轉換爲boolen類型,
//編譯時會提示:不可轉換類型
//boolean b = (boolean)in;
Object obj = "Hello";
//obj變量的編譯時類型爲Object與String存在繼承關係,可以強制類型轉換
//而且obj變量的實際類型是String
String objStr = (String)obj;
System.out.println(objStr);
//定義一個objPri變量,編譯時類型爲Object,實際類型爲Integer
Object objPri = Integer.valueOf(5);
String str = (String)objPri;
}
}
考慮到進行強制類型轉換時可能出現異常,因此進行類型轉換之前應先通過。instanceof運算符來判斷是否可以成功轉換。例如,上面的String str = (String)objPri; 代碼運行時會引發 ClassCastException異常,這是因爲 objPri 不可轉換成 String 類型 爲了讓程序更加健壯,可以將代碼改爲如下:
if (objPri instanceof String)
{
String str = (String)Objpri;
}
在進行強制類型轉換之前,先用 instanceof 運算符判斷是否可以成功轉換,從而避免出現 ClassCastException 異常,這樣可以保證程序更加健壯。
6.9 繼承和組合
繼承是實現類複用的重要手段,但繼承帶來了一個最大的壞處:破壞封裝。組合也是實現類複用的重要方式,而採用組合方式來實現複用則能提供更好的封裝性。
繼承帶來了高度複用的同時,也帶來了嚴重的問題:繼承嚴重地破壞了父類地封裝性。前面介紹封裝時提到:每個類都應該封裝它內部信息和實現細節,而只暴露必要的方法給其他類使用。但在繼承關係中,子類可以直接訪問父類的成員變量和方法,從而創造子類和父類嚴重耦合。
爲了保證父類具有良好的封裝性,不會被子類隨意改變,設計父類通常應該遵循如下規則:
- 儘量隱藏父類的內部數據。儘量把父類的所有成員變量都設置爲private訪問類型,不要讓子類直接訪問父類的成員變量
- 不要讓子類可以隨意訪問、修改父類方法。父類中那些僅爲輔助其他的工具,應該使用private訪問控制符修飾,讓子類無法訪問該方法;如果父類中的方法需要被外部類調用,則必須以public修飾,但又不希望子類重寫該方法,可以使用final修飾符來修飾該方法;如果希望父類的某個方法被子類重寫,但不希望被其他類自由訪問,則可以使用protected來修飾該方法。
- 儘量不要在父類構造器中調用將要被子類重寫的方法
6.9.1 利用組合實現複用
組合是把舊類對象作爲新類的成員變量組合進來,以實現新類的功能。通常需要在新類中使用private修飾被組合的舊類對象。
class Animal
{
private void beat()
{
System.out.println("心臟跳動...");
}
public void breath()
{
beat();
System.out.println("吸一口氣,吐一口氣,呼吸中");
}
}
class Bird
{
// 將原來的父類組合到原來的子類,作爲子類的一個組合成分
private Animal a;
public Bird(Animal a)
{
this.a = a;
}
//重新定義一個自己breath()方法
public void breath()
{
//直接複用Animal提供breath()方法來實現Bird的breath()方法
a.breath();
}
public void fly()
{
System.out.println("我在天空自由飛翔");
}
class Wolf()
{
// 將原來的父類組合到原來的子類,作爲子類的一個組合成分
private Animal a;
public Wolf(Animal a)
{
this.a = a;
}
public void breath()
{
//直接複用Animal提供breath()方法來實現Bird的breath()方法
a.breath();
}
public void run()
{
System.out.println("我在陸地上快速奔跑");
}
}
public class CompositeTest
{
public static void main(String[] args)
{
Animal a1 = new Animal();
Bird b = new Bird(a1);
b.breath();
b.fly();
Animal a2 = new Animal();
Wolf w = new Wolf(a2);
w.breath();
w.fly();
總之,繼承要表達的是一種“是(is-a)”的關係,而組合表達的是“有(has-a)”的關係。
6.10 初始化塊
Java使用構造器來對單個對象進行初始化操作,使用構造器完成整個Java對象的狀態初始化,然後將Java對象返回給程序,從而讓該Java對象的信息更加完整。
6.10.1 使用初始化塊
初始化塊是 Java 類裏可出現的第 種成員(前面依次有成員變量、方法和構造器), 一個類裏可以有多個初始化塊,相同類型的初始化塊之間有順序 前面定義的初始化塊先執行,後面定義的初始化塊後執行。初始化塊的語法格式如下:
[修飾符]{
// 初始化塊可執行代碼
...
}
初始化修飾符只能是static,使用static修飾的初始塊被稱爲靜態初始化塊。
public class Person
{
{
int a = 6;
if (a > 4){
System.out.println("Person初始化模塊a>4");
}
System.out.println("Person第一個初始化模塊a>4");
}
{
System.out.println("Person第二個初始化塊");
}
public Person
{
System.out.println("Person類的無參數構造器");
}
pubilc static void main(String[] args)
{
new Person();
}
}
輸出:
Perso口初始化塊:局部變量 的值大於
Person 的初始化塊
Person 的第 個初始化塊
Person 類的無參數構造器
從運行結果可以看出,當創建 Java 對象時,系統總是先調用該類裏定義的初始化塊,如果一個類裏定義了2個普通初始化塊,則前面定義的初始化塊先執行,後面定義的初始化塊後執行.
6.10.2 初始化塊和構造器
從某種程度上來看,初始化塊是構造器的補充,初始化塊總是在構造器執行之前執行。系統同樣可使用初始化塊來進行對象的初始化操作。
與構造器不同的是,初始化塊是一段固定執行的代碼,它不能接收任何參數 因此初始化塊對同一個類的所有對象所進行的初始化處理完全相同 。基於這個原因,不難發現初始化塊的基本用法,如果有段初始化處理代碼對所有對象完全相同,且無須接收任何參數 ,就可以把這段初始化處理代碼提取到初始化塊中 顯示了把兩個構造器中的代碼提取成初始化塊示意圖。