Java學習之旅基礎知識篇:面向對象之封裝、繼承及多態


    Java是一種面向對象設計的高級語言,支持繼承、封裝和多態三大基本特征,首先我們從面向對象兩大概念:類和對象(也稱為實例)談起。來看看最基本的類定義語法:

/*命名規則:
 *類名(首字母大寫,多個單詞組合時每個單詞首字母大寫,單詞之間不加任何連接符號)
 *字段名、方法名(首字母小寫,多個單詞組合時第一個單詞首字母小寫,之后每個單詞首字母大寫,單詞之間不加任何連接符號)
 */
[public][finalclass 類名 {
    [public|protected|private 類名() {}] //構造器
    [public|protected|private][static|final] 類型 字段名 [=默認值];//字段列表
    [public|protected|private][static|final|abstract] 返回值 方法名(參數) {} //方法列表
}

從以上的語法中發現幾個知識點:(1).構造器名稱為類名相同且沒有任何返回值(甚至都不能返回void);(2).類的修飾符要么為public,要么沒有;(3).字段可以添加默認值;(4).方法的修飾符中final和abstract不能同時使用;(5).字段與方法都可以使用static進行修飾。基於這些,我先解釋1,3,5,剩下的將在后續的講解中逐漸涉及到。首先為什么構造器沒有任何返回值呢?實質上,在Java中當利用new來調用構造器時是有返回值的,總是返回當前類的實例,因此無須定義返回值類型,但也不能顯式使用return來返回,因為構造器的返回值是隱式的。其次,可以在聲明字段的同時為之添加默認值,如未添加系統會自動添加默認值。最后,使用static修飾的字段和方法我們稱為類字段和類方法,可以使用類和實例來調用,但是在類方法中不能訪問任何實例字段和實例方法(即沒有static修飾)。

     利用類的構造器來創建類的實例,如未提供構造器,系統將默認提供一個無參構造器,如提供構造器,系統則不會提供默認構造器(因此自定義構造器的同時建議添加一個無參構造器)。但請注意:構造器雖然是創建對象的重要途徑,但是不完全負責對象的創建,實際上當調用構造器時,系統會為這個對象分配內存空間並執行默認初始化,即在構造器執行之前這個對象就已經產生了,只是還不能被外界訪問,只能在構造器中以this來引用,當構造器執行完畢這個對象作為其返回值返回,可被外界訪問或賦值給其他的引用變量。

class Person {
    public String name;
    public int age;
    
    public Person() {}
    public Person(String n, int a) {
        name = n;
        age = a;
    }
    
    public void display() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}

public class TestPerson {
    public static void main(String[] args) {
        Person p = new Person();
        p.display();//Name: null, Age: 0
        p.name = "Miracle";
        p.age = 28;
        //p = new Person("Miracle", 28);
        p.display();//Name: Miracle, Age: 28
    }
}

與上篇提到過的數組一樣,類也是引用類型,以上Person類定義的引用變量p存放在棧內存中(只是存放實際Person對象的地址,沒有任何實際數據,類似於C的指針),而實際的Person對象存放在堆內存中,下圖顯示了Person對象初始化完畢后的內存分布情況。

因此 對於引用類型來說,棧內存僅僅存放引用類型變量,即實際對象的地址,而堆內存則存放實際對象的數據,通過引用變量來引用實際對象,如果將該引用變量賦值為另一個引用變量,僅僅將兩個引用變量指向同一個對象而已,而不會發生復制對象數據
      提到引用指針,就不得不提到this引用了,它代表當前類正在執行的實例。主要有三種用法:(1).用於區分方法(含構造器)的形參與類字段同名時;(2).用於同一個類實例方法之間調用時;(3).用於同一個類重載構造器之間調用時。來看以下代碼:
class Person {
    public String name;
    public int age;
    
    public Person() {}
    //重載構造器之間調用時,需使用this
    public Person(String name) {
        this(name, 0);
    }
    //構造器中形參與字段同名,需使用this
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    //方法中形參name與字段name同名,需使用this
    public void changeName(String name) {
        this.name = name;
        //方法中還引用實例方法display,此時可使用this,也可不使用
        this.display();
        //display();
    }
    
    public void display() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}

public class TestPerson {
    public static void main(String[] args) {
        Person p = new Person("Miracle", 28);
        p.display();
        p.changeName("Miracle He");
    }
}

  方法是類或對象行為特征的抽象,跟C語言的函數相似,但也有顯著的不同。方法必須屬於類或對象,只能在類中定義,函數則是結構化語言的組成單元。方法可以采用static進行定義,代表這個方法是屬於類或對象,但在static方法中不能調用this或其他非static方法或字段。Java方法的參數傳遞方式只有一種:值傳遞,即將實參的副本傳遞給方法,參數本身不發生任何變化。先看看基本類型參數的傳遞示例:

public class TestPassPrimitiveArgs {
    public static void swap(int a, int b) {
        System.out.println("交換前,a = " + a + ", b = " + b);//a = 3,b = 5
        int temp = a;
        a = b;
        b = temp;
        System.out.println("交換后,a = " + a + ", b = " + b);//a = 5,b = 3
    }
    public static void main(String[] args) {
        int a = 3;
        int b = 5;    
        swap(a, b);
        System.out.println("交換結束后,a = " + a + ", b = " + b);//a = 3,b = 5
    }
}

從運行結果來看,在swap()中交換之前是3和5,交換后變成5和3,而實參在main()中始終變成不變,因此在main()傳遞給swap()的實參只是a和b的副本,而不是a和b本身。我們以內存分布來說明執行狀況,當在main()中傳參給swap()時,實際上就是在main()方法棧區向swap()方法棧區傳遞一份a和b的副本,如下圖:

當執行swap()時,swap()方法棧區將a和b副本進行交換,交換完成后進入main()方法棧區,此時僅僅a和b的副本發生改變,其本身沒有發生任何變化。接下來我們來看看引用類型的交換,前面我們說了只能通過值傳遞的方式來傳參,可能對有些朋友來說稍顯疑惑。

class DataSwap {
    public int a;
    public int b;
}
public class TestPassReferenceArgs {
    public static void swap(DataSwap ds) {
        System.out.println("交換前,ds.a = " + ds.a + ", ds.b = " + ds.b);//ds.a = 3, ds.b = 5
        int temp = ds.a;
        ds.a = ds.b;
        ds.b = temp;
        System.out.println("交換后,ds.a = " + ds.a + ", ds.b = " + ds.b);//ds.a = 5, ds.b = 3
    }
    public static void main(String[] args) {
        DataSwap ds = new DataSwap();
        ds.a = 3;
        ds.b = 5;
        swap(ds);
        System.out.println("交換結束后,ds.a = " + ds.a + ", ds.b = " + ds.b);//ds.a = 5, ds.b = 3
    }
}

從運行結果來看,確實不僅在swap中交換成功,在main中仍然是交換之后的結果。讓人一下覺得:從main中傳遞給swap似乎不是ds對象的副本了,而是ds本身,這與我們前面談到的Java方法傳參只能按值傳遞相違背了,下面我詳細說明一下。我們都知道,此時傳遞的是引用類型DataSwap,而引用類型的內存方式已經談過了,在main()方法棧區中實際存放的是ds對象的地址,而實際的數據(a,b)是存放在堆內存中。現在將ds對象引用由main傳遞給swap,實際上是ds對象的地址復制一份到swap方法棧區中,此時main和swap中都已擁有ds對象的地址,且都指向在堆內存中實際存放的數據。也就是說引用類型參數數據傳遞方式是不折不扣的值傳遞方式,只不過傳遞的僅僅是引用變量,而不是引用變量所指向的引用類型數據。當然這里對main或swap中任何一個ds對象數據的更改都會影響到另一方,同時我們還可以驗證main和swap中的ds是兩個不同的引用變量,試着在swap種方法最后添加: ds=null.也就是切斷swap中對ds的引用,查看一下main中ds對象的a和b是否受到影響(結果是不會)。

接下來,談一談可變參數的實現方式(在類型后添加三個點...),即形參可以輸入任意個參數(類似於C#的params),在看實際例子前,需要說明:可變參數必須是方法的最后一個參數,且最多只有一個可變參數,相當於傳入了一個對應類型的數組(只是長度可變)。

public class TestVarityArgs {
    public static void readBooks(String name, String... books) {
        if(books.length == 0) {
            System.out.println(name + " has not a book to read");
        } else {
            String result = name + " is reading: ";
            for(String book : books) {
                result += book + " ";
            }
            System.out.println(result);
        }
    }
    public static void main(String[] args) {
        readBooks("Miracle");
        readBooks("Miracle", "Java", ".Net", "J2EE");
        readBooks("Miracle", new String[] { "Java", ".Net", "J2EE" });
    }
}

談到了可變參數,似乎跟重載函數非常相似,都是同一個方法有多種調用形式,但是它們有着顯著的區別。重載函數必須滿足"兩同一不同":同一個的重載方法名的必須相同,但是形參列表不同(返回值、修飾符不能作為重載的標准)。請注意,盡量別對包含可變參數的方法進行重載,因為這樣可能會引起歧義。

    public void readBooks(String name) {
        //...
    }
    public void readBooks(String name, String book) {
        //...
    }
    public boolean readBooks(String name, String[] books, int count) {
        //...
    }

我不知道是否有朋友會問返回值為什么不能作為重載的標准呢?假設現在有以下兩個重載方法:int f(){}、void f() {},當執行調用時:int r = f();很明顯是調用前者,但是如果沒有將函數結果賦值呢?直接調用f(),此時你可能就不知道該調用誰了,當然java編譯器比你還糊塗,更不知道怎么辦了,因此返回值不能作為重載的標准。
  前面一直提到static這個概念,接下來我以例子來說明它的應用,可以看出static和非static字段和方法的區別所在。

class Person {
    public String name;
    public int age;
    public static int courses = 2;
    
    public Person() {}
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public static void AddCourse(int count) {
        //在static方法中只能訪問static字段,不能訪問實例字段
        //System.out.println(name + " 's course: ");
        courses += count;
    }
    
    public void display() {
        //在實例方法中可以訪問static字段
        System.out.println("Name: " + name + ", Age: " + age + ", Course: " + courses);
    }
}

public class TestPerson {
    public static void main(String[] args) {
        Person p = new Person("Miracle", 28);
        p.display();
        //static方法可通過類調用,也可通過實例調用,調用效果一致,會對該類的所有實例產生影響
        Person.AddCourse(1);//p.AddCourse(1);
        p.display();
        //p1.courses現在也變成3
        Person p1 = new Person("Miracle He", 28);
        p1.display();
    }
}

那在實際開發中,怎樣例區分static和非static的引用呢?簡單的建議是:如果定義的變量是用來描述每個對象的固有信息(如每個人都有姓名、年齡),則應該使用實例變量,相反如果描述的類的固有信息(如只要是人就只能有兩只眼睛),則應該使用類變量

      不是說面向對象有三大特征嗎?封裝、繼承、多態。那到底是怎么回事呢?首先封裝,就是將對象的屬性等信息隱藏在類的內部,僅提供給外部一些滿足預設條件的方法供調用。拿上面的例子來說明:每個人的年齡只能在0~150之間來進行浮動,現在的情況是我可以隨意更改年齡(想多少歲就多少歲),那肯定就不對了。我們必須將這些不滿足條件的操作及時的過濾掉,Java提供了訪問權限控制: private->default->protected->public(權限依次擴大)來封裝內部屬性和提供外部接口(對字段采用private或protected等修飾符來限制,采用getter和setter來進行有效控制)。

class Person {
    private String name;
    private int age;

    public Person() {}
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        if(name.length() < 2 || name.length() > 20) {
            System.out.println("你設置的名字不合法");
            return;
        } else {
            this.name = name;
        }
    }
    
    public int getAge() {
        return this.age;
    }
    public void setAge(int age) {
        if(age < 0 || age > 100) {
            System.out.println("你設置的年齡不合法");
            return;
        } else {
            this.age = age;
        }
    }
    
    public void display() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}

public class TestAccessControl {
    public static void main(String[] args) {
        Person p = new Person("Miracle", 28);
        p.display();
        //p.age = 150;//此時age已經是private的,無法訪問
        p.setAge(150);//這里已經不合法了,無法修改
        
        p.setName("Miracle He");
        p.setAge(35);
        p.display();
    }
}

關於訪問控制符,有以下建議:類的絕大部分字段(有些少數的static字段需要public修飾)和輔助方法都采用private來修飾,並提供getter和setter訪問器來對其讀取和修改;如果值希望同一個包(即將講到)中其他類訪問,則不添加任何修飾符;如果只希望子類也能使用父類的成員而不被外界知曉,則采用protected來修飾;如果可以在任何地方都能訪問到,則采用public來修飾
     接下來,將討論上文中談到的包。所謂包,就是為不同特征的類隔離起來,即使這些彼此隔離的包中包含同名的類也無所謂(就像在同一個班級中有兩個都叫"Miracle"的同學,老師會叫其中一個"Older Miracle",另一個叫"Little Miracle"來區分)。一般一個類中只能包含在一個包中,且該語句只能為非注釋語句的第一句。

/*命名規則:
 *包名(全部小寫,以公司或項目組織的順序倒寫,中間以.分隔,如: miracle.java.basic)
 *類名(首字母大寫,多個單詞組合時每個單詞首字母大寫,單詞之間不加任何連接符號)
 *字段名、方法名(首字母小寫,多個單詞組合時第一個單詞首字母小寫,之后每個單詞首字母大寫,單詞之間不加任何連接符號)
 */
package 包名;
[public][final] class 類名 [public|protected|private 類名() {}] //構造器 [public|protected|private][static|final] 類型 字段名 [=默認值];//字段列表 [public|protected|private][static|final|abstract] 返回值 方法名(參數) {} //方法列表 }

位於包中的類,在文件系統中也必須保持與包名相同層次的目錄結構。

package miracle.java.basic;
public class Person {
    private String name;
    private int age;
    
    public Person() {}
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public void display() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}
import miracle.java.basic.Person;
public class TestPerson {
    public static void main(String[] args) {
        Person p = new Person("Miracle", 28);
        p.display();
    }
}

我設計的項目組織結構如下圖:

其中實心代表系統真實存在的文件夾和文件,空心代表編譯產生的文件夾和文件。根據開篇中講解的編譯指令來編譯和運行程序。
cd E:\project\src
javac -d ..\bin TestPerson.java
java -classpath ..\bin TestPerson
(或: java -cp ..\bin TestPerson)

運行完畢之后,我們發現源文件和字節碼文件已經徹底分離了,並且在bin文件夾中還生成了跟src文件夾中Person類指定包對應的文件夾目錄(細心的朋友可能發現跟IDE的目錄結構有點相似了,是的我們今天又進步了一點)。在竊喜之余,我們發現在主類中加入了import miracle.java.basic.Person, 這條語句相對於package而言的,正因為我們建立了包,當需要在其他類中引用包中的類時,則需要使用import引入以方便書寫,不然則需要帶全名稱,這樣既繁瑣又不具可讀性。

miracle.java.basic.Person p = new miracle.java.basic.Person("Miracle", 28);

不過有時還必須使用全名稱來書寫。常見的java包:java.lang、java.util、java.net、java.io、java.text、java.sql、java.awt、java.swing等。

import java.sql.*;
import java.util.*;
public class TestPackage {
    public static void main(String[] args) {
        //Date d = new Date();//錯誤,此時真的不明確到底是sql下的Date還是util下的Date
        //正確寫法
        java.sql.Date sd = new java.sql.Date();
        java.util.Date ud = new java.util.Date();
    }
}

此外,Java還是import static語法用來引入指定包下所有的靜態成員。

import static java.lang.System.*;
import static java.lang.Math.*;
public class TestPackage {
    public static void main(String[] args) {
        out.println(PI);
    }
}

再次擴展java類的定義格式:

/*命名規則:
 *包名(全部小寫,以公司或項目組織的順序倒寫,中間以.分隔,如: miracle.java.basic)
 *類名(首字母大寫,多個單詞組合時每個單詞首字母大寫,單詞之間不加任何連接符號)
 *字段名、方法名(首字母小寫,多個單詞組合時第一個單詞首字母小寫,之后每個單詞首字母大寫,單詞之間不加任何連接符號)
 */
package 包名;
import|import static 包名.類名|*;//包名.類名代表僅引入該包指定的類,包名.*代表引入該包所有的類或靜態成員
[public][finalclass 類名 {
    [public|protected|private 類名() {}] //構造器
    [public|protected|private][static|final] 類型 字段名 [=默認值];//字段列表
    [public|protected|private][static|final|abstract] 返回值 方法名(參數) {} //方法列表
}

接下來,我們將探討面向對象的另外兩大特征:繼承和多態。其實這兩者是相互關聯的,繼承就是在已有類的基礎上擴展新的子類,而不改變原有父類的數據和行為,即我們通常所說的父類和子類(遵從"子類 is a 父類"原則),子類可繼承父類的非私有成員(建議將成員修飾符改為protected),同時也可重寫父類相同的成員(遵從"兩同兩小一大"原則:即方法名相同,方法形參相同;子類方法返回類型比父類更小或相等,子類方法拋出的異常比父類更小或相等;子類方法的訪問權限比父類更大或相等,重寫的方法要么都是類方法,要么都是實例方法,如父類有一個實例方法,子類添加一個同名的類方法則不算重寫,而是子類的新方法)。

package miracle.java.basic;
public class Person {
    protected String name;
    protected int age;
    
    public Person() {}
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("調用Person的構造器");
    }
    
    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        if(name.length() < 2 || name.length() > 20) {
            System.out.println("你設置的名字不合法");
            return;
        } else {
            this.name = name;
        }
    }
    
    public int getAge() {
        return this.age;
    }
    public void setAge(int age) {
        if(age < 0 || age > 100) {
            System.out.println("你設置的年齡不合法");
            return;
        } else {
            this.age = age;
        }
    }

    public void display() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}
package miracle.java.basic;
public class Student extends Person {
    private int grade;
    
    public Student() {}
    public Student(String name, int age) {
        super(name, age);
    }
    
    public int getGrade() {
        return this.grade;
    }
    public void setGrade(int grade) {
        if(grade < 0 || grade > 100) {
            System.out.println("你設置的分數不合法");
            return;
        } else {
            this.grade = grade;
        }
    } 
    
    public void display() {
        System.out.println("Name: " + name + ", Age: " + age + ", Grade: " + grade);
    }
    /*
    public static void display() 
    {
       //不能算作重寫,只是Student的新方法
    }
    */
}
import miracle.java.basic.*;
public class TestInheritance {
    public static void main(String[] args) {
        Student miracle = new Student("Miracle", 28);
        miracle.setGrade(85);
        miracle.display();
    }
}

如果在子類中先調用已重寫的父類方法,該怎么辦呢?Java提供了super引用,指向其直接父類的默認引用。當創建一個對象時,系統會隱式創建其父類的對象(Java所有類都繼承自java.lang.Object),只要該類有子類存在,就一定會產生super引用,指向其對應的直接父類,當子類方法中使用某個成員變量時,首先會查找當前類中是否存在,如不存在則查找直接父類中是否存在,如不存在會依次追溯到java.lang.Object中是否存在,如仍然不存在將不能通過編譯。跟前面的this引用很類似,都不能在類方法中引用,只不過this是指向當前子類的對象而已。除此之外,super還能在子類構造器中調用其父類的構造器,如實例化子類的對象,會依次調用所有上層父類的構造器,最后才調用自身的構造器完成對象的構建。如子類調用父類已重寫的類方法,則使用父類.方法()來完成。

package miracle.java.basic;
public class Person {
    protected String name;
    protected int age;
    
    public Person() {}
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("調用Person的構造器");
    }
    
    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        if(name.length() < 2 || name.length() > 20) {
            System.out.println("你設置的名字不合法");
            return;
        } else {
            this.name = name;
        }
    }
    
    public int getAge() {
        return this.age;
    }
    public void setAge(int age) {
        if(age < 0 || age > 100) {
            System.out.println("你設置的年齡不合法");
            return;
        } else {
            this.age = age;
        }
    }

    public void display() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
    
    public static void notice(int age) { if(age >= 18) { System.out.println("今天看電影"); } else { System.out.println("兒童不宜參加"); } } }
package miracle.java.basic;
public class Student extends Person {
    private int grade;
    
    public Student() {}
    public Student(String name) {
        this(name, 0);
    }
    public Student(String name, int age) {
        super(name, age);//只能為第一句且不能跟this混用
        System.out.println("調用Student的構造器");
    }
    
    public int getGrade() {
        return this.grade;
    }
    public void setGrade(int grade) {
        if(grade < 0 || grade > 100) {
            System.out.println("你設置的分數不合法");
            return;
        } else {
            this.grade = grade;
        }
    } 
    
    public void display() {
        super.display();
        System.out.println("Name: " + name + ", Age: " + age + ", Grade: " + grade);
    }
    
    public static void notice(int age) { System.out.print("看電影了:"); Person.notice(age); System.out.println("\n"); } }
import miracle.java.basic.*;
public class TestInheritance {
    public static void main(String[] args) {
        Student miracle = new Student("Miracle", 28);
        miracle.setGrade(85);
        miracle.display();
        Student.notice(miracle.getAge());
        
        Student miracleHe = new Student("Miracle He", 16); 
        miracleHe.display();
        Student.notice(miracleHe.getAge());
    }
}

現在我們將程序做簡單的改動:Person miracle = new Student("Miracle", 28);

Person.java
package miracle.java.basic;
public class Person {
    protected String name;
    protected int age;
    public int height = 150;
    
    public Person() {}
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        if(name.length() < 2 || name.length() > 20) {
            System.out.println("你設置的名字不合法");
            return;
        } else {
            this.name = name;
        }
    }
    
    public int getAge() {
        return this.age;
    }
    public void setAge(int age) {
        if(age < 0 || age > 100) {
            System.out.println("你設置的年齡不合法");
            return;
        } else {
            this.age = age;
        }
    }

    public void display() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}
Student.java
package miracle.java.basic;
public class Student extends Person {
    private int grade;
    public int height = 170;
    
    public Student() {}
    public Student(String name) {
        this(name, 0);
    }
    public Student(String name, int age) {
        super(name, age);
    }
    
    public int getGrade() {
        return this.grade;
    }
    public void setGrade(int grade) {
        if(grade < 0 || grade > 100) {
            System.out.println("你設置的分數不合法");
            return;
        } else {
            this.grade = grade;
        }
    } 
    
    public void display() {
        System.out.println("Name: " + name + ", Age: " + age + ", Grade: " + grade);
    }
}
import miracle.java.basic.*;
public class TestPolymorphic {
    public static void main(String[] args) {
        //miracle編譯時為Person,運行時為Student
        Person miracle = new Student("Miracle", 28);
        //miracle.setGrade(85);
        miracle.display();//方法具備多態性
        System.out.println("Height: " + miracle.height);//150,字段不具備多態性
    }
}

從代碼中可以看出:當引用變量的編譯時類型和運行時類型不一致(父類 t = new 子類();)時,我們說表現出了對象的多態。而多態僅僅表現在調用重寫方法時,將調用子類中的方法,調用非重寫方法時,如果該方法在父類中將調用父類,如果在子類中將無法調用,而對象的字段不具多態性,即只能調用父類中對應的字段。回憶前面幾章講到的數據類型轉換,這里將子類轉化為父類(向上轉型)是成功的;相反向下轉型(父類轉化為子類)時,則不一定成功,必須強制類型轉換,但在轉換之前為了防止出現ClassCastException異常,建議使用instanceof運算符(類似於C#的is)判斷是否成功,如成功才執行強制轉換。

if(miracle instanceof Student) {
    Student stu = (Student)miracle;
    miracle.display();
}

提到繼承,就不得不提到組合這個概念。所謂組合,即將已有類以引用的方式嵌入到另一個類(整體類和部分類之分,遵從"整體類 has a 部分類"原則),以達到在整體類(類似子類)中復用部分類(類似父類)的功能。從類的關系角度來看,繼承是從多個子類中提取共有父類的過程;組合是從多個整體類中提取嵌入類的過程。

Person.java
package miracle.java.basic;
public class Person {
    protected String name;
    protected int age;
    public int height = 150;
    
    public Person() {}
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        if(name.length() < 2 || name.length() > 20) {
            System.out.println("你設置的名字不合法");
            return;
        } else {
            this.name = name;
        }
    }
    
    public int getAge() {
        return this.age;
    }
    public void setAge(int age) {
        if(age < 0 || age > 100) {
            System.out.println("你設置的年齡不合法");
            return;
        } else {
            this.age = age;
        }
    }

    public void display() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}
Club.java
package miracle.java.basic;
public class Club {
    private Person[] members;
    
    public Club() {
        this.members = new Person[]{};
    }
    public Club(Person[] members) {
        this.members = members;
    }
    
    public void playGame() {
        for(Person p : members) {
            p.display();
            if(p.getAge() >= 18) {
                System.out.println("可以上網");
            } else {
                System.out.println("好好學習");
            }
        }
    }
}
import miracle.java.basic.*;
public class TestComposition {
    public static void main(String[] args) {
        Club c = new Club(new Person[] {
            new Person("Miracle", 28),
            new Person("Miracle He", 16)
        });
        c.playGame();
    }
}

可看出Club與Person之間沒有任何相似的關系,只是Club中由很多Person組成,因此是組合關系。

     到現在為止,基本講完了面向對象的三大特性,在結束本篇講解之前,簡單談一下"初始化塊"的應用。所謂初始化塊,就是在構造器執行之前,對整個類(所有對象)的字段進行初始化的過程,通常會將多個構造器中相同的部分放到初始化塊中執行,可以把初始化塊看成是沒有形參的方法,只不過在構造器執行之前執行而已。與構造器執行順序一致,初始化塊也遵循從父類到子類依次執行的過程。與初始化塊對應的還有靜態初始化塊,主要完成類屬性的初始化,並且只在類加載時初始化一次

Parent.java
package miracle.java.basic;
public class Parent {
    static {
        a = 4;
        System.out.println("Parent的靜態初始化塊執行");
    }
    {
        b = 4;

        System.out.println("Parent的初始化塊執行");
    }

    public static int a = 2;
    public int b = 3;
    
    public Parent() {
        System.out.println("Parent的構造器執行");
    }
}
Child.java
package miracle.java.basic;
public class Child extends Parent {
    static {
        //a = 10;
        System.out.println("Child的靜態初始化塊執行");
    }
    {
        b = 6;
        System.out.println("Child的初始化塊執行");
    }
    
    public Child() {
        System.out.println("Child的構造器執行");
    }
}
import miracle.java.basic.*;
public class TestInitBlock {
    public static void main(String[] args) {
        for(int i = 0; i < 2; i++) {
            Child c = new Child();
            System.out.println("a: " + Parent.a + ", b: " + c.b);
        }
    }
}
/* 執行結果:
* Parent的靜態初始化塊執行
* Child的靜態初始化塊執行
* Parent的初始化塊執行
* Parent的構造器執行
* Child的初始化塊執行
* Child的構造器執行
* a: 2, b: 6
* Parent的初始化塊執行
* Parent的構造器執行
* Child的初始化塊執行
* Child的構造器執行
* a: 2, b: 6
*/

從運行結果來看:靜態初始化塊和初始化塊都先於構造器執行,並都遵從父類到子類的執行過程,但靜態初始化塊最先執行且僅執行一次,子類初始化塊在父類的初始化塊和構造器執行完畢之后,在子類構造器之前執行。最后給出完整版的Java類定義格式:

/*命名規則:
 *包名(全部小寫,以公司或項目組織的順序倒寫,中間以.分隔,如: miracle.java.basic)
 *類名(首字母大寫,多個單詞組合時每個單詞首字母大寫,單詞之間不加任何連接符號)
 *字段名、方法名(首字母小寫,多個單詞組合時第一個單詞首字母小寫,之后每個單詞首字母大寫,單詞之間不加任何連接符號)
 */
package 包名;
import|import static 包名.類名|*;//包名.類名代表僅引入該包指定的類,包名.*代表引入該包所有的類或靜態成員
[public][final] class 類名 {
    static {
        //靜態初始化塊
    } 
    {
        //初始化塊
    }
    [public|protected|private 類名() {}] //構造器
    [public|protected|private][static|final] 類型 字段名 [=默認值];//字段列表
    [public|protected|private][static|final|abstract] 返回值 方法名(參數) {} //方法列表
}

OK,到此為止,面向對象(上):封裝、繼承、多態就完全講解完畢,建議讀者把文章中代碼依次執行,領悟其中的要點。下一篇還將繼續面向對象(下):內部類、抽象類及接口。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM