02 Java面向對象

Java作爲一門完全面向對象的語言,筆者感到其威力體現在面向對象的概念大大抽象了程序繁瑣的細節,提取了程序間的共性和聯繫,使得開發者可以投入更多精力在設計程序而不是寫代碼上。由於Java面向對象的內容過多,這裏只列舉一些容易混淆和出錯的地方。畢竟寫技術日記的目的是爲了利於學習,鬍子眉毛一把抓就無法提高效率。

1. 多態

1.1 重載的條件

方法的重載必須滿足以下全部條件:
方法名相同;
參數列表不同;
返回類型可以相同也可以不同;
訪問權限可以相同也可以不同。
其中參數列表不同可以是參數的類型不同,個數不同或者順序不同。因爲方法名和參數列表共同構成了函數簽名(signature),執行某個方法時JVM看到的只是函數簽名。
正確的重載:

public class Test
{
    public static void main(String[] args)
    {
        Add a = new Add();
        System.out.println(a.add(1, 1));
        System.out.println(a.add(1, 1, 1));
    }
}

class Add
{
    public int add(int x, int y)
    {
        return x + y;
    }
    public int add(int x, int y, int z)
    {
        return x + y + z;
    }
}
同樣函數簽名的方法是不允許同時出現在一個類中的,否則JVM無法決定執行哪個方法,比如:

錯誤的重載:

class Add
{
    public int add(int x, int y)
    {
        return x + y;
    }
    public char add(int x, int y) //報錯,已在Add中定義add(int, int)
    {
        return (char)(x + y);
    }
}
正確的重載:

class Add
{
    public int add(int x, int y)
    {
        return x + y;
    }
    public char add(int x, int y, int z)  
    {
        return (char)(x + y + z);
    }
}

兩個add方法參數列表不同,返回值類型也可以不同,扔屬於重載。

1.2 重寫的條件

方法的重寫必須滿足以下全部條件:
子類重寫方法的名稱,參數簽名和返回類型必須與父類原方法的名稱,參數簽名和返回類型一致;
重寫方法的訪問權限大於等於原方法;
重寫方法的異常拋出範圍小於等於原方法;
重寫只能發生在子類和父類之間,重寫方法和原方法必須分別存在於子類和父類中;
重寫只是針對方法,屬性不能被重寫,屬性跟引用變量的聲明類型綁定,
靜態方法不能被重寫,因爲靜態方法跟其被聲明的類綁定。
正確的重寫:
public class Test
{
    public static void main(String[] args)
    {
        Child c = new Child();
        c.display();    //輸出:Child: Child, 調用的是子類的重寫方法
    }
}

class Parent
{
    void display()
    {
        System.out.println(this.getClass().getName() + ": " + "Parent");
    }
}

class Child extends Parent
{
    void display()
    {
        System.out.println(this.getClass().getName() + ": " + "Child");
    }
}

1.3 動態綁定

動態綁定是指一個引用變量調用一個方法時,如果這個方法重寫了其他方法或者被其他方法重寫,則該方法的具體執行內容需要在程序運行時才確定,而且取決於該引用指向的堆內存中的對象類型而非該引用的聲明類型。比如:
public class Test
{
    public static void main(String[] args)
    {
        //PP情況
        Parent pp = new Parent();   //輸出:Parent: Parent
        pp.display();
        //PC情況
        Parent pc = new Child();    //輸出:Child: Child
        pc.display();
        //CP情況
        //Child cp = new Parent();  //報錯:不兼容的類型
        //cp.display();
        //CC情況
        Child cc = new Child();     //輸出:Child: Child
        cc.display();
    }
}

class Parent
{
    void display()
    {
        System.out.println(this.getClass().getName() + ": " + "Parent");
    }
}

class Child extends Parent
{
    void display()
    {
        System.out.println(this.getClass().getName() + ": " + "Child");
    }
}
由上面例子可見,PP和CC情況下引用變量的聲明類型和指向的對象類型一致,則調用其聲明類中的方法。CP情況報錯,因爲屬於子類的聲明類型的引用若要被賦值爲屬於父類的對象類型,需要上轉換,而此處無法進行上轉換。比較複雜的是PC情況,這種情況體現了動態綁定的思想,我們來分析一下:
Parent pc = new Child();
pc.display();
首先,編譯器根據引用的聲明類型(Parent)和方法名(display),搜索相應類(Parent)及其子類(Child)的“方法表”,找出所有名字爲display的方法。可能存在多個方法名爲display的方法,只是參數類型或數量不同。然後,根據函數簽名找出完全匹配的方法。因爲display方法被重寫,所以父子類中的display方法都符合要求。如果display方法的訪問權限爲private,或者訪問修飾符爲static或者final,則立刻可以明確這兩個display方法選哪一個執行(事實上有可能不會出現兩個重名方法),因爲private方法不能被繼承也就不能被重寫,不會出現重名的方法,static方法與類綁定按調用該方法的引用的聲明類型執行,final方法與private方法類似,無法繼承和重寫。經過這三步編譯階段結束。如果在這三步中能確定父子類中重名的display方法(一個是父類的,一個是被子類重寫的)哪一個應當被引用變量調用,則稱其爲靜態綁定。
如果不能確定,則需要在運行階段進行第四步:根據引用變量指向的對象類型來確定調用哪個方法。這裏pc引用變量指向的對象類型是Child類型,所以調用Child類型中重寫後的display方法。

2. 對象

2.1 生命週期

對象有三種生命週期:
離開作用域:
{
    Person p1 = new Person();   //離開作用域時,p1失效,Person對象成爲垃圾
}
引用變量指向null:
{
    Person p1 = new Person();   
    p1 = null;                  //p1失效,Person對象成爲垃圾
}
引用變量的賦值可以延長生命週期:
{
    Person p1 = new Person();
    Person p2 = p1;             //p1, p2兩個引用變量指向同一個Person對象
    p1 = null;                  //p1失效,但p2仍指向Person對象,直到超出作用域後成爲垃圾
}

2.2 比較

有2種方式可用與對象間的比較,== 與equals()方法。==操作符用於比較兩個變量的值(內存地址)是否相等,比如基本數據類型間的比較。equals()方法用於比較兩個對象的內容是否一致:
public class Test
{
    public static void main(String[] args)
    {
        String str1 = new String("abc");
        String str2 = new String("abc");
        String str3 = str1;
        System.out.println(str1 == str2);   //false, 兩個引用分別指向堆內存中的兩個對象
        System.out.println(str1 == str3);   //true, 兩個引用的值相等
    }
}

2.3 構造方法

一個類每創建一個對象,構造方法都會被調用一次。構造方法是public的,亦可爲private(如單態設計模式),與類名相同。構造方法沒有返回值,沒有return語句,可以重載。
特別需要注意的是,如果一個類中輸入了有參構造方法,則其默認生成的無參構造方法不再生成。如果父類使用了有參構造方法,則子類在生成對象時必須用super關鍵字調用父類的有參構造方法,否則子類對象無法生成,比如:
public class Test
{
    public static void main(String[] args)
    {
        Parent p = new Parent(1);
        System.out.println(p);      //輸出1
        Child c = new Child(2);
        System.out.println(c);      //輸出3, 2
    }
}

class Parent
{
    int x;
    Parent(int x)
        //輸入了有參構造方法,默認的無參構造方法不再生成,子類也不可能繼承
    {
        this.x = x;
    }
    @Override
        public String toString()
        {
            return String.valueOf(x);
        }
}

class Child extends Parent
{
    int y;
    Child(int y)    //子類構造方法中調用父類構造方法
    {
        super(3);   //super關鍵字調用父類的有參構造方法,給x賦值
        this.y = y; //子類構造方法給子類獨有的y屬性賦值
    }
    @Override
        public String toString()
        {
            return String.valueOf(x) + ", " + String.valueOf(y);
        }
}

2.4 this關鍵字

this關鍵字是個引用變量,this引用指向調用其所在的方法的對象,也就是當前對象。this主要用在:
區分重名的本類屬性和形參變量:
public class Test
{
    public static void main(String[] args)
    {
        Student stu = new Student("Jack");
    }
}

class Student
{
    String name;
    Student(String name)
    {
        this.name = name;   //this.name代表本類屬性
    }
}
通過this引用把當前的對象作爲一個參數傳遞給其他的方法,通常用於生成一個包含其它對象引用的對象:
public class Test
{
    public static void main(String[] args)
    {
        School sch = new School();
        sch.addStudent();
    }
}

class School
{
    Student stu;
    public void addStudent()
    {
        stu = new Student("Jack", this);    //this將調用本addStudent方法的School對象的引用傳遞給新的Student對象
    }
}

class Student
{
    String name;
    School sch;
    Student(String name, School sch)    //sch引用即是School類中的this
    {
        this.name = name;
        this.sch = sch;
    }
}
構造方法是在產生對象時被Java系統自動調用的,我們不能在程序中像其他調用其他方法一樣去調用構造方法。但是我們可以通過this在一個構造方法裏調用執行其他重載的構造方法:
class Student
{
    String name;
    School sch;
    Student(String name)
    {
        this.name = name;
    }
    Student(String name, School sch)    
    {
        this(name);     //this調用了前面的重載構造方法給name屬性賦了值,本構造方法給sch賦值
        this.sch = sch;
    }
}

3. 參數傳遞

3.1 基本類型變量的參數傳遞

方法的形式參數就相當於方法中的局部變量,方法調用結束時被釋放,並不會影響到主程序中同名的局部變量。 基本類型的變量作爲實參傳遞,並不能改變這個變量的值。例如:
public class Test
{
    public static void main(String[] args)
    {
        int x = 1;          //x存在於main靜態方法的棧區中
        change(x);      
        System.out.println(x);
    }
    public static void change(int x)
    {
        x = 2;              //x存在於change靜態方法的棧區中,不會改變main方法中的x
    }
}
String類型的傳遞規則與基本類型的傳遞規則一致。

3.2 引用類型變量的參數傳遞

Java語言在被給調用方法的參數賦值時,只採用傳值的方式。所以,基本數據類型傳遞的是該數據的值本身,而引用數據類型傳遞的也是這個變量的值本身,即對象的引用(句柄),而非對象本身,但通過方法調用改變了對象的內容,但是對象的引用是不能改變的,所以最終值發生了變量。數組也屬於引用數據類型。
public class Test
{
    public static void main(String[] args)
    {
        int[] arr = {1, 2};                 //引用arr存在於main靜態方法棧區
        change(arr);
        for(int tmp : arr)
        {
            System.out.print(tmp + " ");    //輸出1, 1
        }
    }
    public static void change(int[] arr)
    {
        arr[1] = 1;                         //同一個引用arr
    }
}

4. 內部類

4.1 定義

在一個類內部定義類,這就是嵌套類(nested classes),也叫內部類、內置類。
內部類可以直接訪問嵌套它的類的成員,包括private成員,但是嵌套類的成員卻不能被嵌套它的類直接訪問,比如:
public class Test
{
    public static void main(String[] args)
    {
        Outer.Inner in = new Outer().new Inner();   //先生成外部類的實例,再生成內部類的實例
        in.display();       //輸出outer
    }
}

class Outer
{
    String outer_str = "outer";
    void display()
    {
        //System.out.println(Inner.inner_str);    //報錯:無法在靜態上下文中引用非靜態變量
    }
    class Inner
    {
        String inner_str = "inner";
        void display()
        {
            System.out.println(outer_str);
        }
    }
}
當一個類中的程序代碼要用到另外一個類的實例對象,而另外一個類中的程序代碼又要訪問第一個類中的成員,就應當將另外一個類做成第一個類的內部類。

4.2 外部訪問內部類

雖然外部類的方法不能直接訪問內部類,但可以通過調用內部類對象的方法在外部類之外訪問內部類,只要欲訪問的內部類屬性、方法權限比private高即可,比如:
public class Test
{
    public static void main(String[] args)
    {
        Outer.Inner in = new Outer().new Inner();
        System.out.println(in.inner_str);   //輸出inner
    }
}

class Outer
{
    String outer_str = "outer";
    class Inner
    {
        String inner_str = "inner";
        void display()
        {
            System.out.println(outer_str);
        }
    }
}

4.3 匿名內部類

匿名類是不能有名稱的類,所以沒辦法引用它們。必須在創建時,作爲new語句的一部分來聲明它們。這就要採用另一種形式的new語句,如下所示: new <類或接口> <類的主體> 這種形式的new語句聲明一個新的匿名類,它對一個給定的類進行擴展,或者實現一個給定的接口。它還創建那個類的一個新實例,並把它作爲語句的結果而返回。要擴展的類和要實現的接口是new語句的操作數,後跟匿名類的主體。如果匿名類對另一個類進行擴展,它的主體可以訪問類的成員、覆蓋它的方法等等,這和其他任何標準的類都是一樣的。如果匿名類實現了一個接口,它的主體必須實現接口的方法。比如經常用到匿名內部類的事件監聽程序:
import java.awt.*;   
import java.awt.event.*;   
  
public class QFrame extends Frame 
{   
    public QFrame() 
    {   
           this.setTitle("my application");   
           addWindowListener
           (
                   new WindowAdapter()    //WindowAdapter是個抽象類,匿名內部類繼承了這個抽象類 
                   //匿名內部類開始
                   {    //可以通過new .....()後面緊跟的{}來判斷這是匿名內部類的開始   
                        //重寫WindowAdapter的windowClosing方法
                        public void windowClosing(WindowEvent e)
                        {   
                            dispose();   
                            System.exit(0);   
                        }   
                   }
                   //匿名內部類結束
           );      //有分號    
          this.setBounds(10,10,200,200);   
    }   
}
在使用匿名內部類時,有一些需要特別注意的限制:
匿名內部類不能有構造方法。  
匿名內部類不能定義任何靜態成員、方法和類。  
匿名內部類不能是public,protected,private,static。  
只能創建匿名內部類的一個實例。
一個匿名內部類一定是在new的後面,用其隱含實現一個接口或實現一個類。  
因匿名內部類爲內部類,所以內部類的所有限制都對其生效。
內部類只能訪問外部類的靜態變量或靜態方法。
從上面的分析可以看出,匿名內部類就是繼承了某個抽象類或者接口的內部類的簡化寫法。

5. 總結

面向對象作爲Java語言的核心概念,應當重視。若要深入理解,應該多寫一些試驗程序,並思考相應的內存分配原理,才能從硬件行爲的層面理解軟件的規則。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章