第5章 面向物件上
文章目錄
類用於描述客觀世界裡某一類物件的共有特徵,而物件則是類的具體存在。Java 程式使用類的構造器來建立該類的物件。Java 也支援面向物件的三大特徵: 封裝
類和物件
我們可以把類當做是一種自定義的資料型別,可以使用類來定義變數,這種型別的變數統稱為引用型變數。
定義類
Java 語法裡定義類的簡單語法如下:
[修飾符] class 類名{
零到多個構造器定義......
零到多個屬性......
零到多個方法......
}
類裡各成員之間的定義順序沒有任何影響,各成員之間可以相互呼叫,但需要指出的是,static 修飾的成員不能訪問沒有 static 修飾的成員。
構造器是一個類建立物件的根本途徑,如果一個類沒有構造器,這個類通常將無法建立例項。因此,Java 語言提供了一個功能:如果程式設計師沒有為類編寫構造器,則系統會為該類提供一個預設的構造器。一旦程式設計師為該類提供了構造器,系統將不再為該類提供構造器。
定義屬性
定義屬性的語法格式如下:
[修飾符] 屬性型別 屬性名 [=預設值]
屬性語法格式的詳細說明:
- 修飾符:修飾符可以省略,也可以是 public、protected、private、static、final
- 屬性型別:屬性型別可以是 Java 語言允許的任意資料型別,包括基本型別和引用型別
在 Java 官方說法裡,屬性被稱為 Filed,因此有的地方也把屬性翻譯為欄位。
定義方法
[修飾符] 方法返回值型別 方法名(形參列表){
方法體......
}
方法語法格式的詳細說明:
- 修飾符:修飾符可以省略,也可以是 public、protected、private、static、final、abstract,abstract 和 final 最多隻能出現其中之一
- 方法返回值型別:基本型別和引用型別。
static是特殊的關鍵字,它可用於修飾方法、屬性等成員。static 修飾的成員表明他是屬於這個類共有的,而不是屬於這個類的單個例項。因為,通常把 static 修飾的屬性和方法稱為類屬性和類方法。不使用 static 修飾的屬性和方法則屬於該類的例項,而不屬於該類。因此,把不使用 static 修飾的屬性和方法稱為例項屬性和例項方法。
靜態成員不能直接訪問非靜態成員
定義構造器
[修飾符] 構造器名 (形參列表){
}
實際上,類的構造器是有返回值的,當我們用 new 關鍵字來呼叫構造器時,構造器返回該類的例項,可以把這個類的例項看作是構造器的返回值,因此構造器的返回值型別總是當前類,因此無需定義返回值型別。
物件、引用和指標
如果堆記憶體中的物件沒有任何變數指向該物件,那麼程式將無法訪問該物件,這個物件也就變成了垃圾,Java 的垃圾回收機制將回收該物件,釋放該物件所佔的記憶體區。因此,如果希望垃圾回收機制回收某個物件,只需切斷該物件的所有引用變數和它之間的關係即可,也就把這些引用賦值為 null 即可。
物件的 this 引用
Java 提供了一個 this 關鍵字,this 關鍵字是一個物件的預設引用。this 關鍵字總是指向呼叫該方法的物件。
根據 this 出現的位是不同,this 作為物件的預設引用有兩種情形:
- 構造器中引用該構造器執行初始化物件
- 在方法中引用呼叫該方法的物件
this 關鍵字最大的作用就是讓類中一個方法訪問該類的另一個方法或屬性。
public class Dog {
public void jump(){
System.out.println("正在執行 jump 方法");
}
public void run(){
//想呼叫 jump 方法
Dog dog = new Dog();
dog.jump();
System.out.println("正在執行 run 方法");
}
}
this 可以代表任何物件,當 this 出現在方法體中時,它所代表的物件是不確定的,但它的型別是確定的,它所代表的物件是能是當前類;所以,只有當這個方法被呼叫時,它所代表的物件才能被確定下來:誰在呼叫這個方法,this 就代表誰。
public class Dog {
public void jump(){
System.out.println("正在執行 jump 方法");
}
public void run(){
//想呼叫 jump 方法
// Dog dog = new Dog();
// dog.jump();
this.jump();
System.out.println("正在執行 run 方法");
}
}
在現實世界中,物件的一個方法依賴於另一個方法的現象是十分常見的:例如,吃飯方法依賴於拿筷子方法等等,這種依賴都是同一個物件兩個方法之間的依賴。因此,Java 允許物件的一個成員直接呼叫另一個成員,可以省略 this 字首,這樣寫也是對的。
public class Dog {
public void jump(){
System.out.println("正在執行 jump 方法");
}
public void run(){
//想呼叫 jump 方法
// Dog dog = new Dog();
// dog.jump();
jump();
System.out.println("正在執行 run 方法");
}
}
對於 static 修飾的方法而言,則可以使用類來直接呼叫該方法,如果在 static 修飾的方法中呼叫 this 關鍵字,則
this 無法指向合適的物件,所以,static 修飾的方法不能使用 this 引用【在 static 修飾的方法的方法體中,不能出現 this 關鍵字,因為 this 就指的是當前物件,物件訪問的非靜態成員,既然不能出現 this 關鍵字,那麼 static 修飾的方法自然不能訪問非靜態成員】。由於 static 修飾的方法不能使用 this 引用,所以 static 修飾的方法不能訪問不使用 static 修飾的普通成員。
package com.chao.chapterFive;
public class StaticAccessNonStatic {
public void info(){
System.out.println("簡單的 info 方法");
}
public static void main(String[] args) {
info(); //編譯失敗,因為 main 是靜態方法,而 info 是非靜態方法
}
}
上面編譯錯誤是因為 info() 方法是物件相關的方法,而不是類相關的方法,因此必須使用物件來呼叫該方法。在上面的 main 方法直接呼叫 info 的時候,相當於使用 this 作為該方法的呼叫者,而 static 修飾的方法中不能使用 this 引用,所以程式出現錯誤。如果要呼叫 info ,只能重新建立一個物件。
使用 this 呼叫構造方法
方法詳解
Java 語言裡方法的所屬性主要體現在如下幾個方面:
- 方法不能獨立定義,方法只能在類體內定義
- 從邏輯意義上看,方法要麼屬於一個類,要麼屬於一個物件
- 永遠不能獨立執行方法,執行方法必須使用類或者物件作為呼叫者
方法的引數傳遞機制
package com.chao.chapterFive;
public class TestPrimitiveTransfer {
public static void swap(int a, int b){
int temp = a;
a = b;
b = temp;
System.out.println("swap 方法裡,a 的值是 " + a + " b的值是 " + b);
}
public static void main(String[] args) {
int a = 6;
int b = 9;
swap(a, b);
System.out.println("交換結束後,實參 a 的值是 " + a + " 實參 b 的值是 " + b);
}
}
執行的結果:
swap 方法裡,a 的值是 9 b的值是 6
交換結束後,實參 a 的值是 6 實參 b 的值是 9
值傳遞的實質:當系統開始執行方法時,系統為形參執行初始化,就是把實參變數的值賦給方法的形參變數,方法裡操縱的並不是實際的實參變數。如上面的程式,系統把 main 方法中的實參變數 int a = 6;int b = 9;
賦給 swap 方法中的形參變數 a和b,然後操縱 a 和 b ,不操縱 main 方法中的兩個變數 a 和 b。
方法過載
Java 允許同一個類裡定義多個同名的方法,只要形參列表不同即可。如果同一個類中包含兩個或兩個以上方法的方法名相同,但形參列表不同,則被稱為方法過載。
在 Java 中卻確定一個方法需要三個要素:
- 呼叫者,也就是方法的所屬者,既可以是類,也可以是物件
- 方法名,方法的標識
- 形參列表
方法過載的要求就是兩同、一不同:同一類中方法名相同,引數列表不同。至於方法的其他部分,方法的返回值型別、修飾符等,與方法過載沒有任何關係。
成員變數和區域性變數
- 成員變數是指在類範圍裡定義的變數,也就是前面所說的屬性;區域性變數是指在一個方法內定義的變數。其中,成員變數又被分為類屬性和例項屬性兩種,定義一個屬性時不使用 static 修飾就是例項屬性,使用 static 修飾就是類屬性。其中,類屬性從這個類的準備階段起開始存在,直到系統完全銷燬這個類;而例項屬性從這個類的例項被建立起開始存在,直到系統完全銷燬這個例項。
- 只要類存在,程式就可以訪問該類的類屬性;是要例項存在,程式就可以訪問該例項的例項屬性。
- 與成員變數不同的是,區域性變數除了形參之外,都必須顯示初始化。也就是說,必須給區域性變數和程式碼塊區域性變數指定初始值,否則就不能訪問它們。
- 只要進行了類的初始化,類的成員變數就都會得到一個初值。
成員變數的初始化和記憶體中的執行機制
隱藏和封裝
理解封裝
封裝是面向物件的三大特徵之一(另外兩個是繼承和多型),它指的是將物件的狀態資訊隱藏在物件的內部,不允許外部程式直接訪問物件內部資訊,而是通過該類提供的方法來實現對內部資訊的操作和訪問。
對一個類實現良好的封裝可以實現以下目的:
- 隱藏類實現細節
- 讓使用者只能通過實現預定的方法來訪問資料,從而可以在該方法里加入控制邏輯,限制對屬性的不合理訪問
- 把物件的屬性和實現細節隱藏起來,不允許外界直接訪問
- 把方法暴露出來,讓方法操作或訪問這些屬性
封裝實際上由兩方面的含義:把該隱藏的隱藏起來,把該暴露的暴露出來。這兩方面都需要通過使用 Java 提供的訪問控制符來實現。
使用訪問控制符
Java 提供了三個訪問控制符:public、protected 和 private,分別代表三個訪問控制級別,另外,還有一個不加任何訪問控制符的訪問控制級別,提供了四個訪問控制級別。
訪問控制權限由小到大依次是:private >>> default >>> protected >>> public
- **private:**如果一個類裡的成員使用 private 來修飾,則這個成員只能該類的內部被訪問。很顯然,這個訪問控制符用於修飾屬性最合適,使用它來修飾屬性就可以把屬性隱藏在類的內部。
- **default:**當一個類中的成員或一個頂級類不使用任何訪問控制符修飾,我們就稱它是預設訪問控制,default 訪問控制的成員或頂級類可以被相同包下的其他類訪問。
- **protected:**如果一個成員使用 protected 訪問控制符修飾,那麼這個成員可以被同一包中其他類訪問,也可被不同包中的子類訪問。通常情況下,使用 protected 來修飾一個方法,是希望其子類來重寫這個方法。
- **public:**如果一個成員或頂級類使用 public 修飾,這個成員就可以被所有類訪問,不管是否在同一個包中。
public class Person {
//定義一個例項屬性
private String name;
private int age ;
public void setName(String name){
if(name.length() > 6 || name.length() < 2){
System.out.println("不合規");
return;
}else{
this.name = name;
}
}
public String getName(){
return this.name;
}
public void setAge(int age){
if(age > 100 || age < 0){
System.out.println("年齡不合規");
return;
}
else{
this.age = age;
}
}
public int getAge(){
return this.age;
}
}
被封裝起來的 name 和 age 屬性不能被其它類所直接修改了,只能通過各自對應的 setter 方法來操作 name 和 get。因為允許使用 setter 方法來操控 name 和 age 屬性,所以允許程式設計師在 setter 中新增自己的控制邏輯,從而保證 name 和 age 屬性不會出現與實際不符的情形。
關於訪問控制符的使用,存在以下幾條規則:
- 類裡的絕大多數屬性都應使用 private 修飾
- 如果某個類主要用作其它類的父類,該類裡包含大部分的方法可能僅希望被其子類重寫,而不像被外界直接呼叫,則應該使用 protected 修飾該方法。
package 和 import
Java 的常用包
- java.lang:這個包包含 Java 語言的核心類,如String、Math、System 和 Thread類等,使用這個包下的類無需使用 import 匯入,系統會自動匯入這個包下的所有類。
- java.util:這個包下包含了大量的 Java 工具類/介面 和 集合框架類/介面,例如 Arrays 和 List、Set 等。
- java.net:包含了 Java 網路程式設計相關的類/介面
- java.io:包含了 Java 輸入輸出相關的類/介面
- java.text:包含了一些 Java 格式化相關的類/介面
- java.sql: 包含了 Java 進行 JDBC 資料庫程式設計的相關類/介面
深入構造器
構造器是一個特殊方法,這個特舒方法用於建立類的例項。Java 語言裡構造器是建立物件的重要途徑(即使使用工廠模式、反射等方式建立物件,其實質依然依賴於構造器),因此,Java 類必須包含一個或一個以上的構造器。
使用構造器執行初始化
構造器最大的用處就是在建立物件的時候執行初始化。當建立一個物件時,系統會為這個物件的屬性進行預設初始化,這種預設初始化把所有基本型別的屬性設為 0 或 false,把所有引用型別的屬性設定為 null。
構造器的過載
同一個類裡具有多個構造器,多個構造器的形參列表不同,即被稱為構造器過載。構造器過載允許 Java 類裡包含多個初始化邏輯,從而允許使用不同的構造器來初始化 Java 物件。
如果系統中包含了多個構造器,其中一個構造器執行體裡完全包含另一個構造器的執行體,如構造器B完全包含構造器A,這種情況可在方法B中呼叫方法A。
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.name = name;
// this.color = color;
//通過 this 呼叫另一個過載的構造器的初始化程式碼
this(name, color);
this.weight = weight;
}
}
上面的 Apple 類中包含三個構造器,其中第三個構造器通過 this 呼叫來呼叫另一個構造器的初始化程式碼。使用 this 呼叫另一個過載構造器只能在構造器中使用,而且必須作為構造器執行體的第一條語句。
為什麼要用 this 來呼叫另一個過載的構造器?我們把另一個構造器裡的程式碼複製過來不就行了嗎?
答:如果僅僅從軟體功能的角度來看的話,這麼做確實可以達到同樣的效果。但是軟體開發中有一個規則:不要把相同的程式碼書寫兩次以上。因為軟體是一個需要不斷更新的產品,如果有一天更新的構造器A,假設構造器B,構造器 C… 中都包含了相同的初始化程式碼,則需要同時修改多個構造器;反之,如果我們使用了 this ,則只需修改A即可。
類的繼承
繼承是面向物件的三大特徵之一,也是實現軟體複用的重要手段。Java 的繼承具有單繼承的特點,每個子類只有一個直接父類。
繼承的特點
因為子類是一種特殊的父類,所以父類包含的範圍總比子類大。但是Java 的子類不能獲得父類的構造器。
方法重寫(覆蓋)
這種子類包含父類同名方法的現象被稱為方法重寫,也被稱為方法覆蓋(Override)。可以說子類重寫了父類的方法,也可以說子類覆蓋了父類的方法。方法重寫要遵循“兩同兩小一大”:兩同是指方法名相同、形參列表相同,子類返回值型別要比父類返回值型別更小或相等,子類方法宣告丟擲的異常應比父類方法宣告丟擲的異常更小或相等。子類方法訪問許可權應比父類方法更大或相等。
當子類覆蓋了父類方法後,子類的物件將無法訪問父類中被覆蓋的方法,但還可以在子類方法中呼叫父類中被覆蓋的方法。如果需要在子類方法中呼叫父類被覆蓋的方法,可以使用 super 或父類類名作為呼叫者來呼叫父類中被覆蓋的方法。
父類例項的 super 引用
如果需要在子類方法中呼叫父類被覆蓋的例項方法,可以使用 super 作為呼叫者來呼叫父類被覆蓋的例項方法。super 是 Java 提供的一個關鍵字,它是直接父類物件的預設引用。
Java 程式建立某個類的物件時,系統會隱式建立該類父類的物件。只要有一個子類的物件存在,則一定存在一個與之對應的父類的物件的存在。在子類方法使用 super 引用時,super 總是指向作為該方法呼叫者的子類物件所對應的父類物件。其實,super 引用和 this 引用很像,其中,this 總是指向到呼叫該方法的物件,而 super 則指向 this 指向物件的父物件。
呼叫父類的構造器
子類不會獲得父類的構造器,但是有時候子類構造器裡需要呼叫父類構造器裡的初始化程式碼。在一個構造器裡呼叫另一個過載的構造器使用 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){
// this.size = size;
// this.name = name;
//通過 super 呼叫來呼叫父類構造器的初始化過程
super(size, name);
this.color = color;
}
public static void main(String[] args) {
Sub s = new Sub(5.6, "Java", "red");
System.out.println(s.color);
}
}
從上面的程式中可以看出,使用 super 呼叫和使用 this 呼叫很像,區別在於 super 呼叫的是其父類的構造器,而 this 呼叫的是同一類中過載的構造器。因此,使用 super 呼叫父類構造器也必須出現在子類構造器執行體的第一行,所以 this 呼叫和 super 呼叫不會同時出現。
不管是那種情況,當呼叫子類構造器來初始化子類物件時,父類構造器總會在子類構造器之前執行… 一次類推,建立任何 Java 物件,最先執行的總是 java.lang.Object 類的構造器。
class Creature{
public Creature(){
System.out.println("Creature無參建構函式");
}
}
class Animal extends Creature{
public Animal(String name){
System.out.println("Animal帶一個引數的構造器,該動物的名字為" + name);
}
public Animal(String name, int age){
this(name);
System.out.println("Animal帶兩個引數的構造器,該動物的年齡為" + age);
}
}
public class Wolf extends Animal {
public Wolf(){
super("土狼", 2);
System.out.println("狼的無參構造器");
}
public static void main(String[] args) {
new Wolf();
}
}
執行結果:
Creature無參建構函式
Animal帶一個引數的構造器,該動物的名字為土狼
Animal帶兩個引數的構造器,該動物的年齡為2
狼的無參構造器
從上面的執行結構來看,建立任何物件總是從該類所在繼承樹最頂層類的構造器開始執行,然後依次向下執行,最後才執行本類的構造器。如果某個父類通過 this 呼叫了同類中過載構造器,就會依次執行此父類的多個構造器。
多型
Java 引用變數有兩個型別:一個是編譯時的型別,一個時執行時的型別。編譯時的型別由宣告該變數時使用的型別決定,執行時的型別由實際賦給該變數的物件決定。如果編譯時型別和執行時型別不一致,就會出現所謂的多型**。
因為子類其實是一種特殊的父類,因此 Java 允許把一個子類物件直接賦給一個父類引用變數,無須任何型別轉換,或者被稱為向上轉型,向上轉型由系統自動完成。例如:BaseClass ploymophicBc = new SubClass();,這個ploymophicBc 引用變數的編譯時型別為 BaseClass,而執行時型別為 SubClass,當執行時呼叫該引用變數的方法時,其方法行為總是像子類的方法行為,而不是像父類的方法行為,這將出現相同型別的變數執行同一方法時呈現出不同的特徵,這就是多型。
引用變數的強制型別轉換
編寫 Java 時,引用變數只能呼叫它編譯時型別的方法,而不能呼叫它執行時型別的方法。如果需要讓這個引用變數呼叫它執行時型別方法,則必須將它強制轉換為執行時型別,強制型別轉換需要藉助於型別轉換運算子:(type) veriable
。型別轉換運算子可以將一個基本型別變數轉換為另一個型別,還可以將一個引用型別變數轉換成其子類型別。
- 基本型別之間的轉換隻能在數值型別之間進行,這裡所說的數值型別包括:整數型、字元型和浮點型。但是數值型不能和布林型別之間轉換。
- 引用型別之間的轉換隻能將一個父類變數轉換為子類型別
考慮到強制型別轉換時可能會出現異常,因此在進行強制型別轉換之前應先通過 instanceof 運算子來判斷是否可以轉換成功。
instanceof 運算子
instanceof 運算子的前一個運算元通常是一個引用型別的變數,後一個運算元通常是一個類,它用於判斷**前面的物件是否為後面的類或者其子類、實現類的例項。**如果是則返回 true,否則返回 false。
public class TestInstanceof {
public static void main(String[] args) {
//定義一個編譯型別為Object的物件hello,實際型別是String,因為Object 是所有類的父類,所以可以執行hello instanceof String、
//hello instanceof Math等
Object hello = "Hello";
System.out.println("字串是否為 Object 的例項:" + (hello instanceof Object));
System.out.println("字串是否為 String 的例項:" + (hello instanceof String));
System.out.println("字串是否為 Math 的例項:" + (hello instanceof Math));
String a = "Hello";
//a的編譯型別為 String ,String 既不是 Math 型別,也不是它的父類,所以編譯出錯
//System.out.println("字串是否為 Math 型別的例項:" + (a instanceof Math));
}
}
初始化塊
初始化塊是 Java 類裡可出現的第四種成員(前面依次有屬性、方法和構造器),一個類裡可以有多個初始化塊,相同的型別的初始化塊之間有順序:前面定義的先執行,後面定義的後執行。
public class Person1 {
{
int a = 6;
if(a > 4)
System.out.println("Person初始化塊:區域性變數a的值大於4");
System.out.println("Person的初始化塊");
}
{
System.out.println("Person第二個初始化塊");
}
public Person1(){
System.out.println("類的無參構造器");
}
public static void main(String[] args) {
new Person1();
}
}
執行結果:
Person初始化塊:區域性變數a的值大於4
Person的初始化塊
Person第二個初始化塊
類的無參構造器
從執行結果中可以看出,當建立 Java 物件時,系統總是先呼叫該類裡定義的初始化。初始化塊只在建立 Java 物件時隱式執行,而且在執行構造器之前執行。
初始化塊和構造器
靜態初始化塊
如果定義初始化塊時使用了 static 修飾符,則這個初始化塊就變成了靜態初始化塊,也被稱為類初始化塊。系統將在類初始化階段執行靜態初始化塊,而不是在建立物件時才執行。
靜態初始化塊時類相關的,用於對整個類進行初始化處理,通常用於對類屬性執行初始化處理。靜態初始化塊不能對例項屬性進行初始化處理。
系統在類初始化階段執行靜態初始化塊時,不僅會執行本類的靜態初始化塊,而且會一直追溯到 java.lang.Object 類,先執行 java.lang.Object 的靜態初始化塊,然後執行其父類的…最後才執行該類的靜態初始化塊。