《 Java 編程思想》CH07 複用類

複用代碼是 Java 衆多引人注目的功能之一。java

Java 能夠經過建立類來複用代碼,要在使用類的時候不破壞現有代碼,有兩種方式:app

  • 組合:在新的類中使用現有類的對象。
  • 繼承:按照現有類的類型來建立新類,無需改變現有類的形式,併爲其添加新代碼。

組合語法

  • 使用組合技術只須要將對象引用置於新類中。
  • 每一個非基本類型的對象都有一個 toString() 方法,並且當編譯器須要一個 String 而你傳入一個對象時,toString() 會被調用。
  • 類中的對象引用會被默認初始化爲 null,若是你對其調用任何方法都會拋出異常,可是能夠在不拋出異常的狀況下,仍然能夠打印一個 null 引用
  • 類中對象引用的初始化位置:
    • 在定義對象的地方
    • 在類的構造器中
    • 惰性初始化,即在要使用該對象的地方進行初始化
    • 實例初始化
class Soap {
	private String s;
	Soap() {
		System.out.println("Soup()");
		s = "Constructed";
	}

	@Override
	public String toString() {
		return s;
	}
}

/**
 * Bath
 */
public class Bath {

	private String s1 = "happy",  // 在定義處初始化
					s2; 
	private Soap soap;
	private int i;
	
	public Bath() {
		System.out.println("Inside Bath()");
		soap = new Soap(); // 在構造函數中初四花	
	}

	@Override
	public String toString() {
		if (s2 == null) {
			s2 = "Joy"; // 惰性初始化
		}
		return s2;
	}

	{
		i = 2; // 實例初始化
	}

	public static void main(String[] args) {
		Bath b = new Bath();
		System.out.println(b);
	}
}

繼承語法

  • 繼承是 OOP 語言和 Java 語言不可缺乏的部分,當建立一個類時,老是在繼承,即便沒有顯式繼承某個類,也會隱式地從 Object 類中繼承。
  • 繼承由關鍵詞 extends 指定,其形式如class Detergent extends Cleanser{},基類的全部方法和成員都會自動導入到導出類中。
  • 能夠爲每一個類都建立一個 main 方法,這樣可使得每一個類的單元測試變得簡便。即便某個類只有包訪問權限,其public main也能夠經過 java className的方式訪問到
  • 爲了繼承,通常是將全部的數據成員都指定爲 private,將全部的方法指定爲 public。
  • 咱們對繼承來的方法進行重寫,重寫以後能夠經過 super 關鍵詞訪問基類版本的方法,如super.func();
  • Java 會自動在導出類的構造器中插入對基類構造器的調用,其老是在導出類構造器執行以前,即便是在定義處初始化的語句也會在基類構造器執行以後執行。
  • 即便沒有爲導出類建立構造器,編譯器也會在默認構造器中調用基類的構造器
  • 若是沒有默認的基類構造器,或者想要調用一個帶有參數的基類構造器,就必須使用 super 關鍵詞顯式調用基類構造器,調用基類構造器必須是在你導出類構造器的第一條語句

代理

代理是指,咱們將一個成員對象置於要構造的類中(像組合),但與此同時咱們在新類中暴露該成員對象的全部或部分方法(想繼承)。dom

IDEA自動建立代理的過程:ide

  • 先在代理類中聲明要代理的成員。
  • Alt + Insert快捷鍵,選中 Delegation
  • 選中要代理的函數便可。
class SpaceShipControls {
    void up(int velocity) {}
    void down(int velocity) {}
    void left(int velocity) {}
    void right(int velocity) {}
    void back(int velocity) {}
    void turboBoost() {}
}


public class SpaceShipDelegation {
    SpaceShipControls spaceShipControls = new SpaceShipControls();

    public void up(int velocity) {
        spaceShipControls.up(velocity);
    }

    public void down(int velocity) {
        spaceShipControls.down(velocity);
    }

    public void left(int velocity) {
        spaceShipControls.left(velocity);
    }

    public void right(int velocity) {
        spaceShipControls.right(velocity);
    }

    public void back(int velocity) {
        spaceShipControls.back(velocity);
    }

    public void turboBoost() {
        spaceShipControls.turboBoost();
    }

    public static void main(String[] args) {
        SpaceShipDelegation spaceShipDelegation = new SpaceShipDelegation();
        spaceShipDelegation.left(1);
    }
}

結合使用組合繼承

  • 能夠結合組合和繼承來建立複雜的類
  • 編譯器會強制你去初始化基類,而且要求在構造器最開始出就要這麼作,可是它不會要求你對成員對象進行初始化,所以須要本身注意。
  • Java 中沒有 C++ 中的析構函數,就像以前所說的同樣,若是咱們的類的確須要作一些相似的工做(如關閉文件),咱們須要本身實現一個方法來實現,而當涉及到繼承時,咱們要確保以正確的順序調用該函數,推薦和C++中析構函數的執行順序同樣編寫該函數,即先清理導出類自己,再調用基類的清理函數。
  • 清理函數須要放在 finally 子句中,以防異常的出現,致使清理函數未被執行,可參考練習12
  • 若是 Java 的基類擁有某個已經被屢次重載的方法名稱時,在導出類中從新定義該方法的名稱,不會屏蔽其在基類中的任何版本。這意味着,在導出類中,重載和從新定義(重寫)容易混淆在一塊兒,若是不看基類的定義是很難分辨某個方法是否正確的被從新定義了。咱們可使用@Override註解來標識某個方法咱們但願其是重寫而不是重載,若是一不當心重載了,則會出現編譯錯誤來提醒咱們。

在組合與繼承之間選擇

  • 組合和繼承都容許在新的類中放置子對象,組合是顯式地這樣作,而繼承則是隱式地這樣作。
  • 組合技術一般用於想在新類中使用現有類的功能而非它的接口這種狀況。有時,容許類的用戶直接訪問新類中組合成分是有意義的。
  • 在繼承時,使用某個現有類,開發一個它的特殊版本。一般,這意味着你在使用一個通用類,併爲了某種特殊須要而將其特殊化。

向上轉型

  • 「爲新的類提供方法」不是繼承中最重要的部分,其重要的方面是用來表現新類和基類之間的關係。簡單的說,咱們能夠認爲「導出類是基類的一種類型」,便可以把導出類當成基類來使用
  • 因爲導出類轉換爲基類在繼承圖上是向上移動的,由於咱們將其成爲「向上轉型」
  • 向上轉型是從一個較爲專用的類向較爲通用的類轉變
  • 雖然在教授OOP的過程當中屢次強調繼承,可是咱們應該慎用繼承。判斷是否要使用的繼承的一個簡單方法就是,判斷咱們是否要進行向上轉型,若是要進行向上轉型,則用繼承,反之,則用組合。
class Instrument {
    public void play() {}
    static void tune(Instrument i) {
        i.play();
    }
}

public class Wind extends Instrument {
    public static void main(String[] args) {
        Wind wind = new Wind();
        Instrument.tune(wind); // 傳遞參數時,用了向上轉型
    }
}

final 關鍵字

final 關鍵詞的含義一般指「沒法改變的」,使用這個關鍵詞一般是由於設計和效率的緣由。,final 能夠用在數據、方法和類上。函數

final 數據

  • 數據的恆定不變分爲兩種狀況:編譯時常量和在運行時初始化並並沒有法的改變的值。
  • 在 Java 中,這類常量必須是基本數據類型,而且用關鍵詞 final 表示,並在該常量定義時對其初始化,如final int value = 1。一般,編譯時常量仍是一個static數據,即static final int VALUE_ONE = 1
  • 編譯器常量的命名規則是:全用大寫字母,單詞與單詞之間用_隔開
  • 即便一個變量是final,咱們也沒法肯定其是編譯時常量,由於初始化沒有要求是字面量,即初始化能夠經過調用函數實現,如final int value = rand.nextInt(20)
  • 同時一個final數值,若是其是static的,那麼它多是在類導入時初始化的,而他不是static的話,它是在實例化時初始化的。
  • 對於基本變量,final 使數值恆定不變,可是對於對象引用,其只是要求對象引用不變,即不指向新的對象,而對象自己是能夠被修改的。
  • Java 容許「空白 final」,即被聲明爲 final 可是又沒有給定初值的域,雖然能夠在定義時不給定初值,按時編譯器會保證,final 域在使用前都必須被初始化,即若是沒有在定義處給定 final 域的初值的話,就必須在每一個構造器中對該 final 域進行賦值。
  • Java 容許在參數列表中以聲明的方式將參數指明爲 final,其含義爲,在該函數中沒法修改該變量:
    • 參數類型爲基本類型:能夠讀參數,可是不能修改
    • 參數類型爲對象類型:沒法修改引用

final 方法

  • 能夠將一個方法定義成 final,這樣能夠防止任何繼承類修改它的含義(即導出類沒法覆蓋實現)
  • 在 Java 的早期實現中,對 final 方法的調用會被轉爲內嵌調用(C++ 中的 inline),可是如今不須要用這樣的方式來優化代碼了
  • 類中的全部 private 方法都被隱式的指定爲 final
  • 「覆蓋」只有在方法是基類的接口的一部分時纔會出現,即必須能將一個對象向上轉型爲它的基本類型並調用相同的方法,若是一個方法是 private,那麼它就不是接口的一部分。

final 類

當將一個類的總體定義爲 final 時,就代表該類沒法被繼承,同時隱式地將全部方法都定義爲 final。單元測試

初始化及類的加載

  • 每一個類的編譯代碼都存在與他本身獨立的文件中。該文件只有在須要使用程序代碼的時候纔會被加載。
  • 通常來講,只有在「類首次使用才加載」,即加載發生於第一次建立類的對象或第一次使用類中的靜態域或靜態方法。
  • 在加載導出類是,Java 編譯器會注意到它繼承於某個基類,所以他會先去加載該基類。
package com.company.ch07;

class Insert {
    private int i =  9;
    protected int j;
    Insert() {
        System.out.println("i = " + i + " j = " + j);
        j = 39;
    }
    private static int x1 = printInit("static Insert.x1 init");
    static int printInit(String s) {
        System.out.println(s);
        return 47;
    }
}

public class Beetle extends Insert {
    private int k = printInit("Beetle.k init");
    public Beetle() {
        System.out.println("k = " + k);
        System.out.println("j = " + j);
    }
    private static int x2 = printInit("static Beetle.x2 init");

    public static void main(String[] args) {
        System.out.println("Beetle constructor");
        new Beetle();
    }
}

//    static Insert.x1 init
//    static Beetle.x2 init
//    Beetle constructor
//    i = 9 j = 0
//    Beetle.k init
//    k = 47
//    j = 39

練習

練習1

class Demo {
	public Demo() {
		System.out.println("Demo");
	}
	@Override
	public String toString() {
		return "toString()";
	}
}

/**
 * Ex1
 */
public class Ex1 {
	Demo demo;
	@Override
	public String toString() {
		if (demo == null) {
			demo = new Demo();
		}
		return demo.toString();
	}

	public static void main(String[] args) {
		Ex1 ex1 = new Ex1();
		System.out.println(ex1);
	}
}

練習2

class Cleanser {
	private String s = "Cleanser";
	public void append(String a) {
		s += a;
	}
	public void dilute() { append(" dilute()"); }
	public void apply() { append(" apply()"); }
	public void scrub() { append(" scrub()"); }
	@Override
	public String toString() {
		return s;
	}
	public static void main(String[] args) {
		Cleanser cleanser = new Cleanser();
		cleanser.dilute(); cleanser.apply(); cleanser.scrub();
		System.out.println(cleanser);
	}
}

/**
 * Detergent
 */
public class Detergent extends Cleanser {

	@Override
	public void scrub() {
		append(" Detergent.scrub()");
		super.scrub();
	}

	public void foam() { append(" foam()");}
	public static void main(String[] args) {
		Detergent detergent = new Detergent();
		detergent.dilute();
		detergent.apply();
		detergent.scrub();
		detergent.foam();
		System.out.println(detergent);
		Cleanser.main(args);	
	}
}

class NewDetergent extends Detergent {
	public void scrub() {
		append("NewDetergent");
		super.scrub();
	}
	public void sterilize() {
		append("sterilize");
	}

	public static void main(String[] args) {
		NewDetergent newDetergent = new NewDetergent();
		newDetergent.dilute();
		newDetergent.apply();
		newDetergent.scrub();
		newDetergent.foam();
		newDetergent.sterilize();
		System.out.println(newDetergent);
		Detergent.main(args);
	}
}

// Cleanser dilute() apply()NewDetergent Detergent.scrub() scrub() foam()sterilize
// Cleanser dilute() apply() Detergent.scrub() scrub() foam()
// Cleanser dilute() apply() scrub()

練習3 & 練習4

class Art {
	Art() {
		System.out.println("Art");
	}
}

class Drawing extends Art {
	Drawing() {
		System.out.println("Drawing");
	}
}

/**
 * Cartoon
 */
public class Cartoon extends Drawing{

	// public Cartoon() {
	// 	System.out.println("Cartoon");
	// }

	public static void main(String[] args) {
		new Cartoon();
	}
}

// Art
// Drawing

練習5

class A {
	A() {
		System.out.println("A");
	}
}

class B {
	B() {
		System.out.println("B");
	}
}

class C extends A {
	B b = new B();
	public static void main(String[] args) {
		new C();
	}
}

// A
// B

練習6

class Game {
	Game(int i) {
		System.out.println("Game" + i);
	}
}

class BoardGame extends Game {
	BoardGame(int i) {
		super(i);
		System.out.println("BoardGame");
	}
}

/**
 * Chess
 */
public class Chess extends BoardGame {
	
	Chess() {
		super(11); // 去掉這條語句,會報編譯錯誤
		System.out.println("Chess");
	}
	public static void main(String[] args) {
		new Chess();
	}
}

練習7

class A {
	A(int i) {
		System.out.println("A");
	}
}

class B {
	B(int i) {
		System.out.println("B");
	}
}

class C extends A {
	B b = new B(1);
	C() {
		super(2);
	}
	public static void main(String[] args) {
		new C();
	}
}

練習8

class Game {
	Game(int i) {
		System.out.println("Game" + i);
	}
}

class BoardGame extends Game {
	BoardGame() {
		super(1);
		System.out.println("BoardGame Default");
	}
	BoardGame(int i) {
		super(i);
		System.out.println("BoardGame");
	}
}

練習9

class Component1 {
	Component1() {
		System.out.println("Component1");
	}
}

class Component2 {
	Component2() {
		System.out.println("Component2");
	}
}

class Component3 {
	Component3() {
		System.out.println("Component3");
	}
}

class Root {
	Component1 c1 = new Component1();
	Component2 c2 = new Component2();
	Component3 c3 = new Component3();
	Root() {
		System.out.println("Root");
	}
}

class Stem extends Root {
	Stem() {
		System.out.println("Stem");
	}

	public static void main(String[] args) {
		new Stem();
	}
}
// Component1
// Component2
// Component3
// Root
// Stem

練習10

class Component1 {
	Component1(int i) {
		System.out.println("Component1");
	}
}

class Component2 {
	Component2(int i) {
		System.out.println("Component2");
	}
}

class Component3 {
	Component3(int i) {
		System.out.println("Component3");
	}
}

class Root {
	Component1 c1 = new Component1(1);
	Component2 c2 = new Component2(2);
	Component3 c3 = new Component3(3);
	Root(int i) {
		System.out.println("Root");
	}
}

class Stem extends Root {
	Stem(int j) {
		super(j);
		System.out.println("Stem");
	}

	public static void main(String[] args) {
		new Stem(2);
	}
}

練習11

class DetergentDelegation {
	Detergent detergent = new Detergent();

	public void append(String a) {
		detergent.append(a);
	}

	public void dilute() {
		detergent.dilute();
	}

	public void apply() {
		detergent.apply();
	}

	public void scrub() {
		detergent.scrub();
	}

	public void foam() {
		detergent.foam();
	}

	public static void main(String[] args) {
		Detergent.main(args);
	}
}

練習12

package com.company.ch07;

class Component1 {
	Component1(int i) {
		System.out.println("Component1");
	}
	void dispose() {
		System.out.println("Component1 dispose");
	}
}

class Component2 {
	Component2(int i) {
		System.out.println("Component2");
	}
	void dispose() {
		System.out.println("Component2 dispose");
	}
}

class Component3 {
	Component3(int i) {
		System.out.println("Component3");
	}
	void dispose() {
		System.out.println("Component3 dispose");
	}
}

class Root {
	Component1 c1 = new Component1(1);
	Component2 c2 = new Component2(2);
	Component3 c3 = new Component3(3);
	Root(int i) {
		System.out.println("Root");
	}
	void dispose() {
		System.out.println("root dispose");
		c1.dispose();
		c2.dispose();
		c3.dispose();
	}
}

class Stem extends Root {
	Stem(int j) {
		super(j);
		System.out.println("Stem");
	}
	void dispose() {
		System.out.println("Stem dispose");
		super.dispose();
	}
	public static void main(String[] args) {
		Stem stem = new Stem(2);
		try {
			// do something
		} finally {
			stem.dispose();
		}

	}
}
// Component1
// Component2
// Component3
// Root
// Stem
// Stem dispose
// root dispose
// Component1 dispose
// Component2 dispose
// Component3 dispose

練習13

class Plate {
    Plate(int i) {
        System.out.println("Plate");
    }
    void func(int i) {
        System.out.println("func int " + i);
    }
    void func(double d) {
        System.out.println("func double " + d);
    }
    void func(String s) {
        System.out.println("func string " + s);
    }
}

class DinnerPlate extends Plate {
    DinnerPlate(int i) {
        super(i);
        System.out.println("DinnerPlate");
    }
    void func(char c) {
        System.out.println("func char " + c);
    }

    public static void main(String[] args) {
        DinnerPlate dinnerPlate = new DinnerPlate(1);
        dinnerPlate.func('c');
        dinnerPlate.func("hello");
        dinnerPlate.func(1);
        dinnerPlate.func(1.0);
    }
}
// Plate
// DinnerPlate
// func char c
// func string hello
// func int 1
// func double 1.0

練習14

package com.company.ch07;

class Engine {
    public void start() {}
    public void rev() {}
    public void stop() {}
    void service() {}
}

class Wheel {
    public void inflate(int psi) {}
}

class Window {
    public void rollup() {}
    public void rolldown() {}
}

class Door {
    public Window window = new Window();
    public void open() {}
    public void close() {}
}

public class Car {
    public Engine engine = new Engine();
    public Wheel[] wheels = new Wheel[4];
    public Door left = new Door(), right = new Door();
    
    public Car() {
        for (int i = 0;i < 4; i++) {
            wheels[i] = new Wheel();
        }
    }

    public static void main(String[] args) {
        Car car = new Car();
        car.left.window.rollup();
        car.right.window.rolldown();
        car.wheels[0].inflate(72);
        car.engine.service();
    }
}

練習15

package com.company.ch05;

public class Test {
    protected void func() {}
}
package com.company.ch07;
import com.company.ch05.*;

public class Ex15 extends Test{
    public static void main(String[] args) {
        Ex15 ex15 = new Ex15();
        ex15.func();
    }
}

練習16

class Amphibian {
    void func() {
    }

    static void test(Amphibian amphibian) {
        amphibian.func();
    }
}

public class Frog extends Amphibian {
    public static void main(String[] args) {
        Frog frog = new Frog();
        Amphibian.test(frog);
    }
}

練習17

class Amphibian {
    void func() {
        System.out.println("Amphibian func");
    }

    static void test(Amphibian amphibian) {
        amphibian.func();
    }
}

public class Frog extends Amphibian {

    @Override
    void func() {
        System.out.println("Frog func");
    }

    public static void main(String[] args) {
        Frog frog = new Frog();
        Amphibian.test(frog);
    }
}
// Frog func

練習18

public class Ex18 {
    static Random random = new Random(12);
    final int i = random.nextInt(12);
    static final int j = random.nextInt(12);

    public static void main(String[] args) {
        Ex18 ex18 = new Ex18();
        System.out.println("ex18.i = " + ex18.i);
        System.out.println("ex18.j = " + ex18.j);
        Ex18 ex181 = new Ex18();
        System.out.println("ex181.i = " + ex181.i);
        System.out.println("ex181.j = " + ex181.j);
    }
}
// ex18.i = 8
// ex18.j = 6
// ex181.i = 4
// ex181.j = 6

練習19

public class Ex19 {
    final int k;
    Ex19() {
        k = 1; // 必須賦值
        // k = 2; // 會報錯
    }

    public static void main(String[] args) {
        Ex19 ex19 = new Ex19();
        // ex19.k = 1; // 會報錯
    }
}

練習20

package com.company.ch07;

class WithFinal {
    private final void f() {
        System.out.println("WithFinal.f()");
    }

    private void g() {
        System.out.println("WithFinal.g()");
    }
}

class OverridingPrivate extends WithFinal {
//    @Override //加上註解後編譯錯誤
    private final void f() {
        System.out.println("OverridingPrivate.f()");
    }
//    @Override //加上註解後編譯錯誤
    private void g() {
        System.out.println("OverridingPrivate.g()");
    }
}

class OverridingPrivate2 extends OverridingPrivate {
//    @Override //加上註解後編譯錯誤
    public final void f() {
        System.out.println("OverridingPrivate2.f()");
    }
//    @Override //加上註解後編譯錯誤
    public void g() {
        System.out.println("OverridingPrivate2.g()");
    }
}

public class FinalOverridingIllusion extends OverridingPrivate2 {
    public static void main(String[] args) {
        OverridingPrivate2 overridingPrivate2 = new OverridingPrivate2();
        overridingPrivate2.f();
        overridingPrivate2.g();

        OverridingPrivate overridingPrivate = overridingPrivate2;
//        overridingPrivate.f(); 沒法調用
//        overridingPrivate.g();
        WithFinal withFinal = overridingPrivate;
//        withFinal.f(); 沒法調用
//        withFinal.g();
    }
}

練習21

package com.company.ch07;

class Final {
    final void f() {}
}

public class Ex21 extends Final {
    void f() {} // 編譯出錯
}

練習22

package com.company.ch07;

final class FinalClass {
    
}

public class Ex22 extends FinalClass { //編譯出錯
}

練習23

class Insert {
    private int i =  9;
    protected int j;
    Insert() {
        System.out.println("i = " + i + " j = " + j);
        j = 39;
    }
    private static int x1 = printInit("static Insert.x1 init");
    static int printInit(String s) {
        System.out.println(s);
        return 47;
    }
}

public class Beetle extends Insert {
    private int k = printInit("Beetle.k init");
    public Beetle() {
        System.out.println("k = " + k);
        System.out.println("j = " + j);
    }
    private static int x2 = printInit("static Beetle.x2 init");
    public static int x3 = 3;
    public static void main(String[] args) {
        System.out.println("Beetle constructor");
        new Beetle();
    }
}

class Ex23 {
    public static void main(String[] args) {
        new Beetle();
//        static Insert.x1 init
//        static Beetle.x2 init
//        i = 9 j = 0
//        Beetle.k init
//        k = 47
//        j = 39
        // or
        // System.out.println(Beetle.x3);
//        static Insert.x1 init
//        static Beetle.x2 init
//        3
    }
}

練習24

class Insert {
    private int i =  9;
    protected int j;
    Insert() {
        System.out.println("i = " + i + " j = " + j);
        j = 39;
    }
    private static int x1 = printInit("static Insert.x1 init");
    static int printInit(String s) {
        System.out.println(s);
        return 47;
    }
}

public class Beetle extends Insert {
    private int k = printInit("Beetle.k init");
    public Beetle() {
        System.out.println("k = " + k);
        System.out.println("j = " + j);
    }
    private static int x2 = printInit("static Beetle.x2 init");
    public static int x3 = 3;
    public static void main(String[] args) {
        System.out.println("Beetle constructor");
        new Beetle();
    }
}

class Ex24 extends Beetle {
    public static void main(String[] args) {
        new Ex24();
//        static Insert.x1 init
//        static Beetle.x2 init
//        i = 9 j = 0
//        Beetle.k init
//        k = 47
//        j = 39
    }
}
  1. 調用 Ex24 的main函數(靜態方法),準備加載 Ex24,可是發現其繼承與 Beetle
  2. 準備加載 Beetle,可是發現其繼承與 Insert,所以先加載 Insert
  3. Insert 中的靜態數據先初始化,因此會輸出static Insert.x1 init
  4. Insert 加載並初始化完後,加載 Beetle 並對靜態數據進行初始化,因此會輸出static Beetle.x2 init
  5. 而後加載 Ex24,加載過程完成,調用 main 函數
  6. new Ex24時,實例化的順序爲 Insert -> Beetle -> Ex24
  7. 因此先輸出 Insert 構造函數中的 i = 9 j = 0,之因此 j 爲0,是由於int默認值爲0
  8. 而後在實例化 Beetle 時,先會執行 實例初始化,即private int k = printInit("Beetle.k init");
  9. 最後纔是 Beetle 的構造函數。 > 本文首發於Code & Fun
相關文章
相關標籤/搜索