1. 程式人生 > >Java 基礎回顧:面向物件

Java 基礎回顧:面向物件

1、類

1.1 面向物件

面向物件三大特性

  1. 封裝:客觀事物抽象並封裝成物件,將資料成員、屬性和方法等集合在一個整體內;
  2. 繼承:用於程式碼重用;
  3. 多型:同樣的訊息被不同型別的物件接收時導致完全不同的行為。

1.2 類和物件概述

1.2.1 宣告

宣告形式

[類修飾符] class 類名 [extends 基類] [implements 介面列表]{
     類體
}[;]

說明

No 說明
1 [類修飾符]可選,可為 public, abstract| final, strictfp
2 class 關鍵字,注意首字母小寫
3 [extends基類] 可選,繼承
4 [implements介面列表] 可選:介面
5 [;] 可選

可訪問性:公共類與友好類

No 說明
1 宣告類時使用修飾符 public 修飾的類為公共類,公共類可以被所有其他類訪問,可被同一包中的類直接引用
2 宣告類時使用預設修飾符而沒有 public 的類為友好類,友好類只能被同一包中的類訪問

1.2.2 建立和使用物件

建立物件的語法:

類名 物件名 = new 類名( [引數表] );
類名 物件名; 物件名 = new 類名( [引數表] );

說明:

No 說明
1 訪問要使用點 (.) 運算子,是否可訪問受訪問修飾符的限制
2 Java 中所有的類都是在堆中建立的

物件的比較

new 建立一個類物件將在託管堆中為物件分配一塊記憶體,每個物件有不同的記憶體,代表物件的變數儲存的是存放物件的記憶體的地址,因此即使兩個不同的物件的內容相同,它們也是不相等的。但是,如果將一個物件賦值給另一個物件,那麼,它們的變數都儲存同一塊記憶體的地址,即兩個物件相同,且改變一個物件會影響另一個物件。

1.3 類的成員

1.3.1 靜態成員與例項成員

靜態成員,特徵:

  1. 一般通過類名進行引用:類名.靜態欄位名類名.靜態方法名,用物件訪問時會出錯;
  2. 靜態欄位共享儲存位置,無論建立多少例項,只有一個副本;
  3. 靜態函式成員屬於整個類,故在其程式碼體內不能直接引用例項成員。

例項成員,特徵:

  1. 例項成員必須通過物件例項引用:物件.例項欄位名物件.方法名
  2. 例項欄位屬於類的例項,建立一個物件即分配一塊記憶體區域;
  3. 例項函式成員作用於類的例項,故其方法體內既可使用例項成員,亦可使用靜態成員。

this 關鍵字:

  1. 如果定義的區域性變數和例項欄位重名,可以使用 this 關鍵字引用類的例項欄位。方式:this.例項欄位
  2. 不能在靜態方法中使用 this 關鍵字

初始化順序:

  1. 在類內部,變數定義順的先後決定了初始化的順序,即使變數定義散佈於方法之間,它們仍會在任何方法(包括構造器方法)被呼叫之前得到初始化;
  2. 靜態欄位只有在物件被建立或者第一次訪問靜態資料的時候,它們才會被初始化,而且初始化之後就不會被再次初始化,靜態欄位的初始化要先於例項欄位的初始化;
  3. 靜態初始化程式碼塊和例項程式碼塊的初始化過程與相應的欄位的條件相同;
  4. 初始化的建構函式時,先呼叫最頂層基類的建構函式,逐級向下直到本類的建構函式

示例程式:

// 這裡會被最先呼叫,但是如果我們不加new TestClass()這段,而僅僅宣告一個空的testClass,
// 那麼這裡也依然不會被呼叫
static TestClass testClass = new TestClass();

public static void main(String ...args) {
    TestClass testClass = new TestClass();
}

private static class TestClass {
    private int val0 = say(0);

    private static int sVal0 = staticSay(0);

    static {  System.out.println("靜態初始化程式碼塊"); }

    {  System.out.println("例項初始化程式碼塊");  }

    public TestClass() {
        System.out.println("Constructor()");
    }

    private int val1 = say(1);

    private int say(int i) {
        System.out.println("say(" + i + ")");
        return 0;
    }

    private static int staticSay(int i) {
        System.out.println("static say(" + i + ")");
        return 0;
    }

    private int val2 = say(2);

    private static int sVal1 = staticSay(1);
}

輸出結果:

static say(0)
靜態初始化程式碼塊
static say(1)
say(0)
例項初始化程式碼塊
say(1)
say(2)
Constructor()
----------
say(0)
例項初始化程式碼塊
say(1)
say(2)
Constructor()

1.3.2 欄位

宣告:

[欄位修飾符] 型別 欄位名 [ = 初值 [, 欄位名[ = 初值] ... ]];

欄位修飾符可選: public | protected | private , static , final , transient , volatile

訪問:

物件.例項欄位名;

靜態欄位和例項欄位:

靜態欄位是使用 static 修飾符宣告的欄位,例項欄位是未使用 static 關鍵字宣告的欄位。

宣告:

[欄位修飾符] static 型別 欄位名 [ = 初值 [, 欄位名[ = 初值] ... ]];

訪問:

類名.靜態欄位名;

常量欄位:

[修飾符] final 欄位名 [ =初值 [, 欄位名 [, =初值] ... ] ];

常量只能被賦值一次,否則會出現編譯錯誤. 靜態常量在定義的時候就應該被初始化,例項常量定義時可以不初始化,但是應該在構造方法中初始化。

volatile 欄位和 transient 欄位:

前一個欄位用在多執行緒中,保證了不同執行緒對這個變數進行操作時的可見性;後一個用在序列化中,表示該欄位不用被序列化。

1.3.3 方法

方法的宣告和呼叫:

[方法修飾符] 返回值型別 方法名 ( [ 形參列表 ] ){
   方法體;
}[;]
  1. 方法修飾符可以是 public | protected | private, abstract | final, static, synchronized, native, strictfp
  2. 使用 native 修飾符修飾的方法叫做本地方法,就是指用本地程式設計語言(如 C 或者 C++)來編寫的特殊方法。

方法的呼叫方式

物件.方法名( [引數列表] );

引數的傳遞:

  1. 按值傳遞

    1. 一個方法不能修改基本資料型別的引數;
    2. 一個方法可以改變一個物件引數的狀態;
    3. 一個方法不能讓物件引數引用另一新的物件;
    4. 當傳入的是基本資料型別的時候,無法修改基本資料型別的值,而物件型別引用的值是可以修改的。
  2. 可變形參

    1. 一個方法最多允許一個可變形參,且可變形參只能為方法的最後一個引數.;
    2. 可變形參允許向方法傳遞可變數量的實參(0 或多個)。

方法的過載:

  1. 方法的簽名是由方法的名稱及其引數的數目和型別組成(注意不包含返回型別);
  2. 過載就是相同名稱的,但是引數的型別或者數目不同的不同方法之間構成過載關係。
  3. 對過載方法,如果傳入的實際引數比較大,就將其窄化處理(強制轉換),否則編譯器會報錯;如果傳入的實際引數比較小,就將其提升為,比如 byte 型別可以傳給 int 引數等。

靜態方法和例項方法:

  1. 修飾符:靜態方法使用 staitc 修飾,例項方法不使用 staitc 修飾;
  2. 訪問方式和範圍:靜態方法屬於整個類,只能通過類名訪問:類名.靜態方法名;例項方法屬於某個例項,可以通過 物件名:例項方法名 進行訪問
  3. 許可權:靜態方法只能直接訪問靜態成員;例項方法能夠訪問靜態成員和例項成員。

1.4 物件構造

1.4.1 構造方法

執行類的例項化工作,沒有顯式宣告構造方法時會自動生成一個預設的無參構造方法,並將未賦值的欄位設定為預設值。但是如果聲明瞭帶引數的構造方法,則編譯器不會自動生成預設的無參構造方法,若需要使用無參構造方法則需要顯示地宣告。

也就是說,如果類中提供了至少一個構造器,但沒有提供無參的構造器,則使用無參構造器構造物件就是違法的。而如果沒有定義任何構造器,系統會提供一個無參構造器。

構造方法宣告的形式

[ 修飾符 ] 類名 ( [引數列表] ){
    構造方法體;
}[;]
  1. 修飾符可選,可為 public|protected|private
  2. 類名與構造方法名相同;
  3. 注意構造方法無返回值型別,即使 void 也不行;
  4. 一般構造方法總是 public 的,private 型別的構造方法表明類不能被例項化,常用於只含有靜態成員的類;
  5. 不能顯式呼叫構造方法;
  6. 構造方法中,一般不要做初始化以外的事情;
  7. 可以在一個構造器中呼叫另一個構造器(構造器之間滿足過載關係),方法是使用this指標,即在一個構造器中使用 this(...) 的形式就可以呼叫倆一個構造器;
  8. 如果一個類是另一個類的子類,那麼使用 super(...) 可以呼叫父類的構造方法,且該語句必須是子類構造器的第一條語句;
  9. 還可以使用工廠方法構造例項;
  10. 遇到多個構造引數時考慮使用構建者模式(請參考設計模式部分)

1.4.2 初始化程式碼塊

靜態初始化程式碼塊:

static {
    靜態初始化程式碼塊;
}[;]
  1. 建議一個類只包含一個例項化和靜態初始化程式碼塊;
  2. 初始化程式碼塊用於初始化類所需的操作;
  3. 在建立一個例項或引用任何靜態成員之前,將自動呼叫靜態初始化程式碼塊,執行靜態初始化;在建立一個例項時,將自動呼叫例項化初始化程式碼塊。類的靜態初始化程式碼塊在程式中至多執行一次,而初始化程式碼塊建立一個物件時執行一次。

例項化程式碼塊:

{
    例項化程式碼塊
}[;]

例項化程式碼塊只要構造類的物件,這些塊就會被呼叫。

“析構”關鍵字 finalize:

在 C++ 中,對每個函式除了有建構函式還有一個解構函式,在 Java 中是沒有解構函式的,不過可以在一個方法的前面新增 finalize 關鍵字賦予一個方法析構的功能。即新增 finalize 關鍵字的方法會在垃圾回收器清除物件之前呼叫,但儘量不要使用它,因為無法確定它何時會被呼叫。

1.7 類成員訪問修飾符

修飾符 同一個類 同一包 不同包,子類 不同包,非子類
public Yes Yes Yes Yes
protected Yes Yes Yes
預設 預設 Yes Yes
private Yes
  1. 如果我們希望一個類成員只由該類的子類使用,那麼我們將其定義為 protected 型別;
  2. 如果我們希望一個類成員只在該類的內部使用,那麼我們將其定義為 private 型別;
  3. 如果我們希望一個類成員可由任何類訪問,那麼我們將其定義為 public 型別;
  4. 如果我們希望一個類成員只在該類所處的包中使用,那麼我們將其定義為預設的型別。

1.8 巢狀類

  1. 在一個類中定義的類. 巢狀類分為巢狀頂級類和內部類. 內部類又分例項內部類、本地內部類和匿名內部類;
  2. 在內部類(非靜態)中訪問外部類的成員可以使用 外部類名.this.成員名字 的形式。

巢狀頂級類:

  1. 關鍵字 static 修飾的內部類;

  2. 巢狀頂級類屬於類的靜態成員,與具體的物件例項無關;

  3. 巢狀頂級類的成員方法可以直接訪問其頂級類的所有靜態成員,但不能訪問頂級類的非靜態成員;

  4. 宣告並建立巢狀頂級類的方法:

     頂級類名.巢狀頂級類名 巢狀頂級類物件 = new 頂級類物件.巢狀頂級類名();
    

例項內部類:

  1. 宣告時前面沒有加static關鍵字修飾的巢狀類為例項內部類;

  2. 例項內部類的成員方法可訪問其頂級類的所有成員.(包括private成員);

  3. 宣告並建立例項內部類的方法:

     頂級類名.例項內部類名 例項內部類物件 = new 頂級類物件.例項內部類名();
    

本地內部類:

  1. 在一個類的方法中定義的類為本地內部類,本地內部類可以訪問頂級類的靜態成員和例項成員外,還可以訪問其所在的方法中的區域性變數和方法的引數;
  2. 本地內部類不能使用 private 和 public 等修飾,作用域侷限於區域性類的塊中;
  3. 如果在本地內部類中訪問外部類或者方法的變數成員,則必須將被訪問的變數成員定義為 final 的。

匿名內部類:

匿名內部類沒有顯式的類名,宣告的同時,必須使用 new 關鍵字建立其物件例項。

1.10 物件的生命週期

1.10.1 物件的建立

使用 new 關鍵字建立物件,建立物件的方法:

  1. 使用 new 建立物件,如 String s = new String(“abc”);
  2. 使用反射建立物件,如 Object s2 = Class.forName(“String”);
  3. 呼叫物件的 clone() 方法,這種方法不會呼叫建構函式;
  4. 使用反序列化方法;
  5. 隱式建立,如 String s3 = ”abc”; String s4 = s3 + “xyz”;

1.10.2 物件的使用

1.10.3 物件的銷燬

Java語言使用垃圾回收器 (GC) 機制來銷燬物件. 可以通過呼叫 System.gc() 強制執行垃圾回收器.

2、繼承和多型

2.1 繼承概述

  1. 將超類賦值給子類之前應該先進行 instanceof 檢查,但應儘量少用,因為這會帶了額外的開銷;
  2. 將子類的引用賦值給超類變數,編譯器是允許的;
  3. 將超類的引用賦值給子類變數,必須進行強制型別轉換;
  4. 被final修飾的類無法被繼承,類中的方法也會預設為 final 的,即無法被子類覆寫。

2.2 繼承

2.2.1 派生類的宣告

派生類使用 extends 關鍵字指定要繼承的基類:

[ 類修飾符 ] class 類名 [ extends 基類 ] {
    類體;
}

注:Java不支援多重繼承。

2.2.2 super 關鍵字

使用 super 關鍵字從派生類訪問基類的成員:

  1. 指定建立派生類例項時應呼叫的基類構造方法:super(引數)
  2. 呼叫基類上已經被其他方法重寫的方法:super.方法(引數)
  3. 訪問基類的資料成員:super.欄位名

注:不能在靜態方法中使用 super 關鍵字.

2.2.3 構造方法的繼承和呼叫

如果使用派生類的其他構造方法構造物件例項,則必須在派生類的構造方法的第一條語句中,顯式地使用 super 關鍵字呼叫其基類的構造方法,否則會編譯錯誤。

2.2.4 類成員變數的隱藏

如果派生類中聲明瞭與繼承的成員變數名同名的成員變數,則該重名成員變數將隱藏從基類繼承的同名成員變數,成為成員變數的隱藏。若需要引用從基類中繼承的同名成員變數,可使用 super 關鍵字. 即:super.變數名

2.2.5 類方法的重寫

  1. 全新方法:與基類方法的 方法名和引數列表(引數型別或者個數)均不同;
  2. 過載方法:與基類方法的 方法名相同,但是引數列表(引數型別或者個數)不同;
  3. 重寫方法:與基類方法的 方法名和引數列表(引數型別或者個數)均相同。

注意:

  1. 當基類的方法被重寫,而又要在派生類中呼叫基類的方法時,可以使用super關鍵字super.方法名
  2. 雖然返回型別不是方法簽名的一部分,但是派生類中重寫的方法的返回型別必須與基類中被重寫的方法相同;
  3. 派生類中重寫的方法不能縮小基類中被重寫方法的訪問許可權,許可權順序:public protected private
  4. 基類的非靜態方法不能被派生類的靜態方法重寫,基類的靜態方法不能被派生類的非靜態方法重寫;
  5. 基類的final方法不能被派生類重寫;
  6. 基類的abstract方法必須被派生類重寫,否則派生類也必須為 abstract

2.2.6 抽象類和抽象方法

抽象類:

  1. 使用 abstract 修飾的類為抽象類;
  2. 抽象類不能被例項化,對抽象類使用 new 關鍵字會導致錯誤. 但是可以將子型別的例項賦值給抽象的超類,比如
    Person p = new Student(“Name”,id); 其中 Person 是超類,Student 是繼承了 Person 的子類;
  3. 允許(但不要求)抽象類包含抽象成員. 也就是抽象類可以包含具體資料和具體方法,甚至不包含抽象方法;
  4. 抽象類不能被密封,即不能被 final 修飾;
  5. 當從抽象類派生非抽象類時,非抽象類必須實現所繼承的所有抽象成員.否則子類必須為抽象的。

抽象方法:

  1. 使用 abstract 關鍵字修飾的方法;
  2. 抽象方法不提供該方法的任何實際實現;
  3. 除構造方法、靜態方法和私有方法不能宣告為抽象的外,其他任何方法均可宣告為抽象方法;
  4. 如果派生類從基類繼承了抽象方法,則要麼重寫以實現所有抽象方法,要麼使用關鍵字 abstract 宣告派生類為抽象類。

2.2.7 密封類和密封方法

密封類:

  1. 將關鍵字 final 置於 class 前面即可將類宣告為密封類;
  2. 密封類不能作為其他類的基類,因此它不能是抽象的。

密封方法:

  1. 使用 final 修飾的方法,也稱為最終方法;
  2. 使用 final 修飾可以防止派生類進一步重寫該方法;
  3. 可將密封類和抽象類、密封方法和抽象方法看作對立的,即:抽象的類必須被繼承,抽象的方法必須被重寫;密封的類禁止被繼承,密封的方法禁止被重寫。

2.3 多型

實現多型有兩種方法:

  1. 方法過載(靜態繫結):編譯時確定;
  2. 方法重寫(動態繫結):執行時確定。

只有普通的方法是多型的,欄位不是多型的:

public static void main(String ...args) {
    Sub sub = new Sub();
    System.out.println("sub.field = " + sub.filed);
    System.out.println("((Super) sub).filed = " + ((Super) sub).filed);
    System.out.println("sub.getField() = " + sub.getFiled());
    System.out.println("((Super) sub).getFiled() = " + ((Super) sub).getFiled());
    System.out.println("sub.getSuperField() = " + sub.getSuperField());
}

private static class Super {
    public int filed = 0;

    public int getFiled() {
        return filed;
    }
}

private static class Sub extends Super {
    public int filed = 1;

    @Override
    public int getFiled() {
        return filed;
    }

    public int getSuperField() {
        return super.filed;
    }
}

輸出結果:

sub.field = 1
((Super) sub).filed = 0
sub.getField() = 1
((Super) sub).getFiled() = 1
sub.getSuperField() = 0

這裡為 Super.field 和 Sub.field 分配了不同的儲存空間,在 Sub 中實際上包含了兩個名為 filed 的域:它自己定義的和基類中的。引用 Sub 中的 filed 時預設會引用到 Sub.field,如果要訪問 Super.field,必須顯式地使用 super.field.

3、介面

  1. 介面本身不提供任何方法的實現;
  2. 繼承介面的任何非抽象類都必須實現介面的所有成員;
  3. 介面類似於抽象基類,不能被例項化.介面不是類,不能用 new 例項化;
  4. 介面中所有方法預設為 public 和 abstract(所有方法都是公共抽象的);
  5. 介面可以包含抽象方法和靜態常量欄位;
  6. Java 不支援多重繼承,但是類和結構可以從多個介面繼承. 介面本身可從多個介面繼承。

3.1 介面的宣告

介面宣告

[ 介面修飾符 ] interface 介面名 [ extends 基介面列表 ] {
    介面體;
}[;]

3.2 介面成員

  1. 介面只能包含抽象方法和靜態常量欄位;
  2. 介面成員變數預設為靜態常量欄位 (public static final),但介面成員變數不能帶除 public static final 以外任何修飾符;
  3. 介面成員方法預設為公共抽象方法 (public abstract),但介面成員方法不能帶除 public abstract 以外任何修飾符。

3.3 介面實現

派生類使用 implements 關鍵字指定要實現的基介面列表:

[ 類修飾符 ] class 類名 [ implements 基介面列表 ]{
    類體;
}

3.4 介面繼承

介面從零或多個介面繼承,被繼承的介面稱為該介面的基介面. 介面使用關鍵字 extends 指定要繼承的介面。故實現該介面的類必須實現介面本身以及該介面的基類的成員:

[ 介面修飾符 ] interface [ extends 基介面列表 ]{
    類體;
}