1. 程式人生 > 實用技巧 >【Java基礎】面向物件下

【Java基礎】面向物件下

面向物件下

這一章主要涉及其他關鍵字,包括 this、super、static、final、abstract、interface、package、import 等。

static

在 Java 類中,可用 static 修飾屬性、方法、程式碼塊、內部類。

特點:

  • 隨著類的載入而載入,由於類只會載入一次,則靜態變數在記憶體中也只會存在一份,存在方法區的靜態域中;
  • 優先於物件存在;
  • 修飾的成員,被所有物件所共享;
  • 訪問許可權允許時,可不建立物件,直接被類呼叫。

注意

  • 在靜態的方法中,不能使用 this 關鍵字和 super 關鍵字;
  • 關於靜態屬性和靜態方法的使用,要從生命週期的角度去理解。

單例模式

所謂的單例模式,就是採取一定的方法保證在整個系統中,對某個類只能存在一個物件例項。

單例模式的好處:

  • 某些類建立比較頻繁,對於一些大型的物件,這是一筆很大的系統開銷;
  • 省去了 new 操作符,降低了系統記憶體的使用頻率,減輕了 GC 壓力;
  • 保證獨立性等。

實現:

對比:

  • 餓漢式的壞處是物件載入時間過長,好處是執行緒安全的;
  • 懶漢式的好處是延遲物件的載入,壞處是執行緒不安全的;

應用:

  • 網站的計數器,一般也是單例模式實現,否則難以同步;
  • 應用程式的日誌應用,一般都使用單例模式實現,這一般是由於共享的日誌
    檔案一直處於開啟狀態,因為只能有一個例項去操作,否則內容不好追加;
  • 資料庫連線池的設計一般也是採用單例模式,因為資料庫連線是一種資料庫
    資源;
  • 專案中,讀取配置檔案的類,一般也只有一個物件。沒有必要每次使用配置
    檔案資料,都生成一個物件去讀取;
  • Application 也是單例的典型應用;
  • Windows 的 Task Manager (工作管理員) 就是很典型的單例模式;
  • Windows 的 Recycle Bin (回收站) 也是典型的單例應用。在整個系統執行過程中,回收站一直維護著僅有的一個例項。

理解 main 方法的語法

由於 JVM 需要呼叫類的 main() 方法,所以該方法的訪問許可權必須是
public,又因為 JVM 在執行 main() 方法時不必建立物件,所以該方法必須是 static

的,該方法接收一個 String 型別的陣列引數,該陣列中儲存執行 Java命令時傳遞給所執行的類的引數

又因為 main() 方法是靜態的,我們不能直接訪問該類中的非靜態成員,必須創
建該類的一個例項物件後,才能通過這個物件去訪問類中的非靜態成員。

程式碼塊

程式碼塊(或初始化塊)的作用:對 Java 類或物件進行初始化。

一個類中程式碼塊若有修飾符,則只能被 static 修飾,稱為靜態程式碼塊
(static block),沒有使用 static 修飾的,稱為非靜態程式碼塊。

靜態程式碼塊:用 static 修飾的程式碼塊

  • 可以有輸出語句;
  • 可以對類的屬性、類的宣告進行初始化操作;
  • 不可以呼叫非靜態的屬性和方法;
  • 若有多個靜態程式碼塊,那麼按照從上到下的順序依次執行;
  • 靜態程式碼塊隨著類的載入而載入,且只執行一次。靜態程式碼塊的執行要先於非靜態程式碼塊。

非靜態程式碼塊:沒有 static 修飾的程式碼塊

  • 可以有輸出語句;
  • 可以對類的屬性、類的宣告進行初始化操作;
  • 可以呼叫非靜態和靜態的屬性和方法;
  • 若有多個非靜態程式碼塊,那麼按照從上到下的順序依次執行;
  • 每次建立物件的時候,都會執行一次。非靜態程式碼塊的執行要先於構造器。
package com.parzulpan.java.ch04;

/**
 * @Author : parzulpan
 * @Time : 2020-11-22
 * @Desc : 程式碼塊練習
 */

class Root{
    static{
        System.out.println("Root的靜態初始化塊");  // 1
    }

    {
        System.out.println("Root的普通初始化塊");  // 4
    }

    public Root(){
        System.out.println("Root的無引數的構造器"); // 5
    }
}

class Mid extends Root{
    static{
        System.out.println("Mid的靜態初始化塊");   // 2
    }

    {
        System.out.println("Mid的普通初始化塊");   // 6
    }

    public Mid(){
        System.out.println("Mid的無引數的構造器");  // 7
    }

    public Mid(String msg){
        //通過this呼叫同一類中過載的構造器
        this();
        System.out.println("Mid的帶引數構造器,其引數值:"
                + msg); // 8
    }
}

class Leaf extends Mid{
    static{
        System.out.println("Leaf的靜態初始化塊");  // 3
    }

    {
        System.out.println("Leaf的普通初始化塊");  // 9
    }

    public Leaf(){
        //通過super呼叫父類中有一個字串引數的構造器
        super("尚矽谷");
        System.out.println("Leaf的構造器"); // 10
    }
}

public class LeafTest{
    public static void main(String[] args){
        new Leaf(); // 輸出結果順序看註釋
    }
}
package com.parzulpan.java.ch04;

/**
 * @Author : parzulpan
 * @Time : 2020-11-22
 * @Desc : 程式碼塊練習
 */


class Father {
    static {
        System.out.println("11111111111");  // 1
    }

    {
        System.out.println("22222222222");  // 2
    }

    public Father() {
        System.out.println("33333333333");  // 3

    }

}

public class Son extends Father {
    static {
        System.out.println("44444444444");  // 4
    }

    {
        System.out.println("55555555555");  // 5
    }

    public Son() {
        System.out.println("66666666666");  // 6
    }

    public static void main(String[] args) { // 由父及子 靜態先行
        System.out.println("77777777777");  // 7
        System.out.println("************************"); // 8
        new Son();  // 輸出結果:1 -> 4 -> 7 -> 8 -> 2 -> 3 -> 5 -> 6
        System.out.println("************************");

        new Son();  // 輸出結果:2 -> 3 -> 5 -> 6
        System.out.println("************************");
        new Father();   // 輸出結果:2 -> 3
    }

}

總結:由父及子,靜態先行。

程式中成員變數賦值的執行順序:

  • 宣告成員變數的預設初始化;
  • 顯式初始化、多個初始化塊一次被執行(同級別下按先後順序執行);
  • 構造器再對成員進行初始化操作;
  • 通過“物件.屬性”或“物件.方法”的方式,可多次給屬性賦值。

final

在 Java 中宣告方法變數時,可使用關鍵字 final 來修飾,表示“最終的”。

  • final 標記的類不能被繼承。比如 String類、System類、StringBuffer類;
  • final 標記的方法不能被子類重寫。比如 Object 類中的 getClass()
  • final 標記的變數(成員變數或區域性變數)稱為常量(名稱大寫,且只能被賦值一次)。比如 final double MY_PI = 3.14;

注意:final 標記的成員變數必須 在宣告時在每個構造器中程式碼塊中 顯式賦值,然後才能使用。

static final 用來修飾屬性,則稱為全域性常量。

抽象類與抽象方法

  • 用 abstract 關鍵字修飾一個類,這個類叫做抽象類;
    • 抽象類不能被例項化;
    • 抽象類是用來被繼承的,抽象類的子類必須重寫父類的抽象方法,並提供方法體,此時才可例項化。若沒有重寫全部的抽象方法,仍為抽象類,需要使用 abstract 修飾;
    • 抽象類中一定有構造器,便於子類例項化呼叫(涉及子類物件例項化的全過程)。
  • 用 abstract 關鍵字修飾一個方法,這個方法叫做抽象方法;
    • 抽象方法:只有方法的宣告,沒有方法的實現,以分號結束。比如:public abstract void talk();
    • 含有抽象方法的類必須被宣告為抽象類。

注意

  • 不能用 abstract 修飾變數、程式碼塊、構造器;
  • 不能用 abstract 修飾私有方法、靜態方法、final 的方法、final 的類。
package com.parzulpan.java.ch04;

/**
 * @Author : parzulpan
 * @Time : 2020-11-22
 * @Desc : 抽象類舉例
 */

public class AbstractTest {
    public static void main(String[] args) {
        AA aa = new BB();
        aa.m1();
        aa.m2();
    }
}

abstract class AA {
    abstract void m1();

    public void m2() {
        System.out.println("A類中定義的m2方法");
    }
}

class BB extends AA {
    void m1() {
        System.out.println("B類中定義的m1方法");
    }
}

模版方法模式

抽象類體現的就是一種模板模式的設計,抽象類作為多個子類的通用模板,子類在抽象類的基礎上進行擴充套件、改造,但子類總體上會保留抽象類的行為方式。

解決的問題:

  • 當功能內部一部分實現是確定的,一部分實現是不確定的。這時可以把不確定的部分暴露出去,讓子類去實現。
  • *換句話說,在軟體開發中實現一個演算法時,整體步驟很固定、通用,這些步驟已經在父類中寫好了。但是某些部分易變,易變部分可以抽象出來,供不同子類實現。這就是一種模板模式。
package com.parzulpan.java.ch04;

/**
 * @Author : parzulpan
 * @Time : 2020-11-22
 * @Desc : 抽象類的應用:模版方法模式
 */

public class TemplateMethodTest2 {
    public static void main(String[] args) {
        BankTemplateMethod bankTemplateMethod = new DrewMoney();
        bankTemplateMethod.process();
        BankTemplateMethod bankTemplateMethod1 = new ManageMoney();
        bankTemplateMethod1.process();
    }

}

abstract class BankTemplateMethod {

    private int id;
    private static int init = 0;

    BankTemplateMethod() {
        super();
        id = init++;
    }

    // 具體方法
    public void takeNumber() {
        System.out.println("取號排隊 " + this.id);
    }
    public void evaluate() {
        System.out.println("反饋評分\n");
    }

    // 鉤子方法,辦理具體的業務
    public abstract void transact();

    // 模版方法,把基本操作組合到一起,子類一般不能重寫
    public final void process() {
        this.takeNumber();
        this.transact();
        this.evaluate();
    }
}

class DrewMoney extends BankTemplateMethod {
    @Override
    public void transact() {
        System.out.println("我要取款");
    }
}

class ManageMoney extends BankTemplateMethod {
    @Override
    public void transact() {
        System.out.println("我要理財");
    }
}

模板方法設計模式是程式設計中經常用得到的模式。各個框架、類庫中都有他的影子,比如常見的有:

  • 資料庫訪問的封裝;
  • Junit 單元測試;
  • JavaWeb 的 Servlet 中關於 doGet/doPost 方法呼叫;
  • Hibernate 中模板程式;
  • Spring 中 JDBCTemlate、HibernateTemplate 等。

interface

為什麼要有介面:

  • 一方面,有時必須從幾個類中派生出一個子類,繼承它們所有的屬性和方法。但是,Java 不支援多重繼承。有了介面,就可以得到多重繼承的效果。
  • 另一方面,有時必須從幾個類中抽取出一些共同的行為特徵,而它們之間又沒有 is-a 的關係,僅僅是具有相同的行為特徵而已。例如:滑鼠、鍵盤、印表機、掃描器、攝像頭、充電器、MP3機、手機、數碼相機、行動硬碟等都
    支援USB連線。

介面就是規範,定義的是一組規則,體現了現實世界中“如果你是/要...則必須能...”的思想。繼承是一個 "是不是" 的關係,而介面實現則是 "能不能"
的關係。

介面(interface)是抽象方法常量值定義的集合。

介面的特點:

  • 用 interface 來定義,和 class 是並列關係;
  • 介面中的所有成員變數都預設是由 public static final 修飾的;
  • 介面中的所有抽象方法都預設是由 public abstract 修飾的;
  • 介面中沒有構造器,意味著介面不能例項化;
  • 介面採用多繼承機制,介面讓類去實現(implements)的方式來使用:
    • 如果實現覆蓋了介面的所有抽象方法,則此實現類就可以例項化;
    • 如果實現類沒有覆蓋介面中的所有抽象方法,則此實現類仍未一個抽象類。
  • 一個類可以實現多個介面。語法class SubClass extends SuperClass implements InterfaceA, InterfaceB {}
  • 介面也可以繼承其它介面,並且可以多繼承。語法interface AA extends BB, CC {}
  • 與繼承關係類似,介面與實現類之間存在多型性

抽象類和介面的異同:

區別點 抽象類 介面
定義 包含抽象方法的類 主要是抽象方法和全域性常量的集合
組成 構造器、抽象方法、普通方法、常量、變數 抽象方法、常量
使用 子類繼承抽象類(extends) 子類實現介面(implements)
關係 抽象類可以實現多個介面 介面不能繼承抽象類,但允許繼承多個介面
常用設計模式 模版方法 簡單工廠、工廠方法、代理
物件 都不能直接例項化,都必須通過物件的多型性產生例項化物件
侷限 抽象類有單繼承的侷限 介面沒有此侷限
實際 作為一個模板 是作為一個標準或是表示一種能力
選擇 如果抽象類和介面都可以使用的話,優先使用介面,因為避免單繼承的侷限
// 如何定義介面:JDK7及以前,只能定義全域性常量和抽象方法。
// 全域性常量:public static final,書寫時可省略;
// 抽象方法:public abstract,書寫時可省略;

public interface Flyable {
    public static final int MAX_SPEED = 7900;
    init MIN_SPEED = 1;

    public abstract void fly();
    void stop();
}

class Plane implements Flyable {
    public void fly() {
        System.out.println("通過引擎起飛");
    }

    punlic void stop() {
        System.out.println("通過減速停止");
    }
}

// 如何定義介面:JDK8,除了能定義全域性常量和抽象方法外,還能定義靜態方法、預設方法。

Java8 介面新特性

JDK8,除了能定義全域性常量和抽象方法外,還能定義靜態方法、預設方法。

靜態方法:使用 static 關鍵字修飾。只能通過介面直接呼叫靜態方法,並執行其方法體。我們經常在相互一起使用的類中使用靜態方法。你可以在標準庫中找到像 Collection/Collections 或者 Path/Paths 這樣成對的介面和類。

預設方法:預設方法使用 default 關鍵字修飾。可以通過實現類物件來呼叫。我們在已有的介面中提供新方法的同時,還保持了與舊版本程式碼的相容性。比如:java 8 API 中對 Collection、List、Comparator 等介面提供了豐富的預設方法。

public interface AA {
    double PI = 3.14;

    public default void method() {
        System.out.println("北京");
    }

    default String method1() {
        return "上海";
    }

    public static void method2() {
        System.out.println(“hello lambda!");
    }
}

若一個介面中定義了一個預設方法,而另外一個介面中也定義了一個同名同
引數的方法(不管此方法是否是預設方法),在實現類同時實現了這兩個接
口時,會出現介面衝突。解決辦法:實現類必須覆蓋介面中同名同參數的方法,來解決衝突。

若一個介面中定義了一個預設方法,而父類中也定義了一個同名同參數的非
抽象方法,則不會出現衝突問題。因為此時遵守類優先原則。介面中具有相同名稱和引數的預設方法會被忽略。

代理模式

代理模式是 Java 開發中使用較多的一種設計模式。代理設計就是為其他物件提供一種代理以控制對這個物件的訪問。

信用卡是銀行賬戶的代理, 銀行賬戶則是一大捆現金的代理。 它們都實現了同樣的介面, 均可用於進行支付。 消費者會非常滿意, 因為不必隨身攜帶大量現金; 商店老闆同樣會十分高興, 因為交易收入能以電子化的方式進入商店的銀行賬戶中, 無需擔心存款時出現現金丟失或被搶劫的情況。

package com.parzulpan.java.ch04;

/**
 * @Author : parzulpan
 * @Time : 2020-11-22
 * @Desc : 介面的應用:代理模式
 */

public class ProxyTest {

    public static void main(String[] args) {
        Star s = new Proxy(new RealStar());
        s.confer();
        s.signContract();
        s.bookTicket();
        s.sing();
        s.collectMoney();
    }
}

interface Star {
    void confer();// 面談

    void signContract();// 籤合同

    void bookTicket();// 訂票

    void sing();// 唱歌

    void collectMoney();// 收錢
}

// 代理類
class RealStar implements Star {

    public void confer() {
    }

    public void signContract() {
    }

    public void bookTicket() {
    }

    public void sing() {
        System.out.println("明星:歌唱~~~");
    }

    public void collectMoney() {
    }
}

// 被代理類
class Proxy implements Star {
    private Star real;

    public Proxy(Star real) {
        this.real = real;
    }

    public void confer() {
        System.out.println("經紀人面談");
    }

    public void signContract() {
        System.out.println("經紀人籤合同");
    }

    public void bookTicket() {
        System.out.println("經紀人訂票");
    }

    public void sing() {
        real.sing();
    }

    public void collectMoney() {
        System.out.println("經紀人收錢");
    }
}

應用場景:

  • 安全代理:遮蔽對真實角色的直接訪問;
  • 遠端代理:通過代理類處理遠端方法呼叫(RMI);
  • 延遲載入:先載入輕量級的代理物件,真正需要再載入真實物件。

代理分類:

  • 靜態代理(靜態定義代理類);
  • 動態代理(動態生成代理類)。

內部類

當一個事物的內部,還有一個部分需要一個完整的結構進行描述。而這個內部的完整的結構又只為外部事物提供服務,那麼整個內部的完整結構最好使用內部類。

在 Java 中,允許一個類的定義位於另一個類的內部,前者稱為內部類,後者稱為外部類。

內部類的分類:

  • 成員內部類(static 成員內部類 和 非 static 成員內部類);
  • 區域性內部類(不談修飾符,方法內、程式碼塊內、構造器內)、匿名內部類。

成員內部類作為類的成員

  • 可以呼叫外部類的結構;
  • 可以被 static 修飾;
  • 可以被 4 中許可權修飾符修飾。

成員內部類作為類

  • 可以在內部定義屬性、方法、構造器等結構;
  • 可以宣告為 abstract 的 ,因此可以被其它的內部類繼承;
  • 可以宣告為 final 的,即不能被其它類繼承;
  • 編譯以後生成 OuterClass$InnerClass.class 位元組碼檔案。
class Person {
    // 靜態成員內部類
    static class A {

    }

    // 非靜態成員內部類
    class B {

    }

    Person() {
        class E {

        }
    }

    public void method() {
        class C {

        }
    }

    {
        class D {

        }
    }
}

注意

練習和總結


以下程式碼的執行情況?

interface A {
    int x = 0;
}
class B {
    int x = 1;
}
class C extends B implements A {
    public void pX() {
        // System.out.println(x);
        System.out.println(super.x);    // 更改 1
        System.out.println(A.x);    // 更改 0
    }
    public static void main(String[] args) {
        new C().pX();
    }
}

編譯出錯,x 屬性不明確。


以下程式碼的執行情況?

interface Playable {
    void play();
}

interface Bounceable {
    void play();
}

interface Rollable extends Playable, Bounceable {
    Ball ball = new Ball("PingPang");
}

class Ball implements Rollable {
    private String name;
    public String getName() {
        return name;
    }
    public Ball(String name) {
        this.name = name;
    }
    public void play() {
        ball = new Ball("Football");
        System.out.println(ball.getName());
    }

interface Rollable 裡的 ball 是 全域性常量 public static final,Ball 中的 play() 重新給 ball 賦值了。