Java程式設計(2021春)——第三章類的重用筆記與思考

被水淹沒的一條魚 發表於 2021-07-20
Java

Java程式設計(2021春)——第三章類的重用筆記與思考

本章概覽:

3.1 類的繼承(概念與語法)

3.2 Object類(在Java繼承最頂層的類)

3.3 終結類和終結方法(只能拿來用,不可以繼承)

3.4 抽象類(有一些方法暫時不能實現)

3.5 泛型

3.6 類的組合(繼承是隸屬關係,組合不是)


3.1.1&3.1.2 類繼承的概念與語法

繼承是一種以已有類為基礎,生成設計新類的機制,是物件導向程式設計的重要特徵。

類繼承的概念

  1. 根據已有類來定義新類,新類擁有已有類的所有功能。
  2. Java中只支援類的單繼承,每個子類只能有一個直接超類。
  3. 已有的作為基礎的類叫做超類,也叫做基類父類
  4. 由超類產生的新類叫做子類(subclass,在物件導向概念中也稱派生類
  5. 超類是所有子類的公共屬性及方法的集合,子類則是超類的特殊化。
  6. 繼承機制可以提高程式的抽象程度,提高程式碼的可重用性。

超類和子類

子類物件與超類物件存在“是一個...”(或“是一種...”)的關係,將在後續例題中介紹。

子類物件

從外部來看,他應該包括:

  1. 與超類相同的介面,即,子類應當有超類所有的對外介面對外服務。
  2. 可以具有更多的方法和資料成員。

其內包含著超類的所有變數和方法,但是子類的物件裡面儲存的只有從超類繼承來的屬性和自己本類所擴充套件新增的屬性(類的方法只會存放在方法,不會在每個物件中儲存,物件中進儲存資料)。

繼承的語法

/*類繼承關係的說明*/
[ClassModifier]class ClassName extends SuperClassName{
    //類體
}

類繼承舉例:

設有三個類:Person,Emploee,ManagerPerson是最廣泛的概念,EmploeePerson更具體一些,Manager又是比泛泛的Manager更具體的類。因此,可以讓Emploee繼承Person,再讓Manager繼承Emploee,用自然語言可以描述為Emploee是一種PersonManager是一種Emploee,這其中便蘊含了類的繼承關係設計的思想。

例:類繼承

public class Person{
    public String name;
    public String getName(){
        return name;
    }
}
public class Employee extends Person{
    public int employeeNumber;
    public int getEmployeeNumber(){
        return employeeNumber;
    }
}
public class Manager extends Emploee{
    public String responsibilities;
    public String getResponsibilities(){
        return responsibilities;
    }
}

測試類

public class Exam4_2Test {
	public static void main(String[] args) {
		Employee li = new Employee();
		li.name = "Li Ming";//從超類繼承的屬性
		li.employeeNumber = 123456;//自己的屬性
		System.out.println(li.getName());//從超類繼承的方法
		System.out.println(li.getEmploeeNumber());//自己定義的方法

		Manager he = new Manager();
		he.name = "He Xia";//從超類的超類繼承的屬性
		he.employeeNumber = 543469;//從超類繼承的樹屬性
		he.responsibilities = "Internet Project";//自己的屬性
		System.out.println(he.getName());//從間接超類繼承的方法
		System.out.println(he.getEmploeeNumber());//從直接超類繼承的方法
		System.out.println(he.getResponsibilities());//自己定義的方法
	}
}

執行結果

Li Ming
123456
He Xia
543469
Internet Project

例:訪問從超類繼承的成員

public class classB {
	public int a = 10;
	private int b = 20;
	protected int c = 30;

	public int getB() {
		return b;
	}
}
public class classA extends classB {
	public int d;

	public void tryVariables() {
		System.out.println(a);//允許
		System.out.println(b);//不允許
		System.out.println(getB());//允許
		System.out.println(d);//允許
	}
}

從上例可知,classA中直接訪問a是可以的,但試圖直接訪問b是不可以的,因為private是私有屬性,在繼承類中不可以直接訪問(可以檢視我的《Java程式設計(2021春)——第二章筆記與思考》這篇blog中有具體介紹許可權控制範圍),此時可以檢視超類中是否為該private設定訪問介面,此處有即為getB()


3.1.3 隱藏和覆蓋

子類對從超類繼承而來的屬性和行為可以重新定義:定義重名的屬性,則從超類繼承過來的屬性會被隱藏;如果宣告一個和超類繼承過來的方法圓形一模一樣的方法,那麼從超類繼承的方法會被覆蓋

屬性的隱藏

子類中宣告瞭與超類中相同的成員變數名

  1. 從超類繼承的變數將被隱藏,但仍存在。
  2. 子類擁有了兩個相同名字的變數,一個繼承自超類,另一個由自己宣告。
  3. 當子類執行繼承自超類的操作時,處理的是繼承自超類的變數,而當子類執行它自己宣告的方法時,所操作的是它自己宣告的變數
  4. 變數隱藏可以修改型別

例:

class Parent {
	Number aNumber;
}
class Child extends Parent {
	Float aNumber;
}

通過這樣的辦法可以隱藏掉超類中的某些屬性,從而定義我們自己的屬性。

訪問被隱藏的超類屬性

  1. 呼叫從超類繼承的方法,則操作的是從超類繼承的屬性。
  2. 本類中宣告的方法使用super.屬性訪問從超類繼承的屬性。
public class A1 {
	int x = 2;
	public void setx(int i) {
		x = i;
	}
	void printa() {
		System.out.println(x);
	}
}
public class B1 extends A1 {
	int x = 100;

	void printb() {
		super.x = super.x + 10;//操作從超類繼承而來的x
		System.out.println("super.x=" + super.x + " x= " + x);
	}
}
public class Exam4_4Test {
	public static void main(String args[]) {
		A1 a1 = new A1();
		a1.setx(4);
		a1.printa();

		B1 b1 = new B1();
		b1.printb();
		b1.printa();

		b1.setx(6);//將繼承x的值設定為6
		b1.printb();
		b1.printa();
		a1.printa();
	}
}

輸出:

4
super.x=12 x= 100
12
super.x=16 x= 100
16
4

例:訪問超類靜態屬性

class A {
	static int x = 2;//注意,雖然可以被所有子類物件成員訪問,但是靜態成員不被繼承

	public void setx(int i) {
		x = i;
	}

	void printa() {
		System.out.println(x);
	}
}
class B extends A {
	int x = 100;

	void printb() {
		super.x = super.x + 10;
		System.out.println("super.x=" + super.x + " x= " + x);
	}
}
public class Tester {
	public static void main(String args[]) {
		A a1 = new A();
		a1.setx(4);
		a1.printa();
		B b1 = new B();
		b1.printb();
		b1.printa();
		b1.setx(6);
		b1.printb();
		b1.printa();
		a1.printa();
	}
}

輸出:

4
super.x=14 x= 100
14
super.x=16 x= 100
16
16

注意,靜態成員不屬於任何一個物件,不會有多個副本,所以在Tester類中呼叫的方法對super.x進行操作時,父類A中的x就是被改變了,從別的物件訪問也是被改變的。

方法的覆蓋

  1. 如果子類不需要使用從超類繼承來的方法的功能,則可以宣告自己的同名方法,稱為方法覆蓋
  2. 覆蓋方法的返回型別、方法名稱、引數的個數以及型別必須和被覆蓋的方法一模一樣
  3. 只需在方法名前面使用不同的類名或不同類的物件名即可區分覆蓋方法和被覆蓋方法。
  4. 覆蓋方法的訪問許可權可以比被覆蓋的寬鬆,但是不能更為嚴格。例如,方法在超類中是公有的,在子類中也必須是公有的,不能比這個更嚴格;方法在超類中是個保護的或私有的,則在子類中可以是公有的。

方法覆蓋的應用場合

  1. 子類中實現與超類相同的功能,但採用不同的演算法或公式。
  2. 在名字相同的方法中,要做比超類更多的事情。
  3. 在子類中需要取消從超類繼承的方法。

方法覆蓋的注意事項

必須覆蓋的方法
  1. 派生類必須覆蓋基類中的抽象的方法,否則派生類自身也成為抽象類。抽象類不能生成例項,只能當作超類用。(稍後會詳細介紹抽象類)
不能覆蓋的方法
  1. 基類中宣告為final的終結方法。
  2. 基類中宣告為static的靜態方法。
呼叫被覆蓋的方法

super.overriddenMethodName();


3.2 Object

  1. Java中Object類是所有類的直接或間接的超類,處在類層次的最高點
  2. 包含了所有Java類的公共屬性。

Object類的主要方法

  1. public final Class getClass() :獲取當前物件所屬的類資訊,返回Class物件。
  2. public String toString() :返回表示當前物件本身有關資訊的字串物件。
  3. public boolean equals(Object obj) :比較兩個物件引用是否指向同一物件,是則返回true ,否則返回false
  4. protected Object clone() : 複製當前物件,並返回這個副本
  5. public int hashCode() : 返回該物件的雜湊程式碼值。
  6. protected void finalize()throws Throwable : 在物件被回收時執行,通常完成資源的釋放工作。

相等和同一

  1. 相等(equal):兩個物件具有相同的型別及相同的屬性值。
  2. 同一(identical):兩個引用變數指向的是同一個物件
  3. 兩個物件同一,則肯定相等。
  4. 兩個物件相等,不一定同一。
  5. 比較運算子==判斷的是這兩個物件是否同一。

例:用==判斷兩個引用是否同一

public class Tester1 {
	public static void main(String[] args) {
		BankAccount a = new BankAccount("Bob", 123456, 100.00f);
		BankAccount b = new BankAccount("Bob", 123456, 100.00f);
		if (a == b) {
			System.out.println("YES");
		} else {
			System.out.println("NO");
		}
	}
}

輸出:

NO

解析:判斷連個引用是否相等,本質上是判斷他們指向的物件是否相同。

equals方法

Objct類中的equals()方法的定義如下:

public boolean equals(Object x){
    return this == x;
}

例:equals方法

public class EqualsTest {
	public static void main(String[] args) {
		BankAccount a = new BankAccount("Bob", 123456, 100.00f);
		BankAccount b = new BankAccount("Bob", 123456, 100.00f);
		if (a.equals(b)) {
			System.out.println("YES");
		} else {
			System.out.println("NO");
		}
	}
}

輸出:

NO

由上可知,equals()方法的功能天然的是判斷物件是否同一,而非判斷兩個物件是否相等。

因此,當我們想要判斷兩個物件是否相等而非物件是否相同時,需要自己寫equals()方法體覆蓋Object類中的equals()方法。

例:覆蓋equals()方法(1)

BankAccount類中覆蓋equals()方法

public boolean equals(Object x) {
	if (this.getClass() != x.getClass())
		return false;//先判斷同類
	BankAccount b = (BankAccount) x;//強轉未知型別的x(其實程式執行到這說明x的型別是符合要求的)到BankAccount類
	return ((this.getOwnerName().equals(b.getOwnerName())) && (this.getAccountNumber() == b.getAccountNumber())&& (this.getBalance() == b.getBalance()));
	}

例:覆蓋equals()方法2

public class Apple {
	private String color;
	private boolean ripe;

	public Apple(String aColor, boolean isRipe) {
		color = aColor;
		ripe = isRipe;
	}

	public void setColor(String aColor) {
		color = aColor;
	}

	public void setRipe(boolean isRipe) {
		ripe = isRipe;
	}

	public String getColor() {
		return color;
	}

	public boolean getRipe() {
		return ripe;
	}

	public String toString() {
		if (ripe)
			return ("A ripe" + color + "apple");
		else
			return ("A not so ripe" + "apple");
	}
    public boolean equals(Object obj) {
		if(obj instanceof Apple) {
			Apple a = (Apple)obj;
			return ( (color.equals(a.getColor())) && (ripe == a.getRipe()))
		}
		return false;
	}
}

上述採用instanceof來判obj是否型別相符合。

hashCode()方法

hashCode()是一個返回物件雜湊碼的方法,該方法實現的一般規定是:

  1. 在一個Java程式的一次執行過程中,如果物件”相等比較“所使用的資訊沒有被修改的話,同一物件執行hashCode()方法每次都應返回到同一個整數。在不同的執行中,物件的hashCode()方法返回值不必一致。
  2. 如果依照equals()方法兩個物件是相等的,則在這兩個物件上呼叫hashCode()方法應該返回同樣的整數結果。
  3. 如果依據equals()方法兩個物件不相等,並不要求在這兩個物件上呼叫hashCode()方法返回值不同。

只要實現得合理,Object類定義的hashCode()方法為不同物件返回不同的整數。一個典型的實現是,將物件的內部地址轉換為整數返回,但是Java語言並不要求必須這樣實現。

clone方法

用於根據已存在的物件構造一個新的物件,即複製物件。

使用clone方法複製物件

  1. 覆蓋clone方法:在Object類中被定義為protected,所以需要覆蓋為public

  2. 實現Clonable介面,賦予一個物件被克隆的能力(cloneability)。不需要再額外去實現什麼,只是一個標記表示可以克隆。

    例:如下表示允許MyObject類被克隆

    class MyObject implements Cloneable{
        //...
    }
    

finalize方法

  1. 在物件被垃圾回收器回收之前,系統自動呼叫物件的finalize方法,但是該方法不能夠被顯式地呼叫。一個物件不再被使用即隨時有可能被回收,但是什麼時刻回收、以什麼次序回收並沒有規定。
  2. 如果要覆蓋finalize方法,覆蓋方法的最後必須呼叫super.finalize

getClass方法

  1. getClass是一個final方法,返回一個Class物件,用來代表物件所屬的類。
  2. 通過Class物件,可以查詢類的各種資訊:如名字、超類、實現介面的名字等。

例:

void PrintClassName(Object obj){
    System.out.println("The Object's Class is "+obj.getClass().getName());
}

notify,notifyAll,wait方法

  1. final方法,不能覆蓋
  2. 這三個方法主要應用在多執行緒程式中。

3.3 終結類與終結方法

  1. final修飾的類和方法。
  2. 終結類不能被繼承。
  3. 中介方法不能被子類覆蓋。

例:終結類

宣告ChessAlgorithm類為final

final class ChessAlgorithm{...}

如果再寫如下程式:

class BetterChessAlgorithm extends ChessAlgorithm{...}

編譯器將顯示一個錯誤

Chess.java:6:Can't subclass final classes:class ChessAlgorithm

例:終結方法

final方法舉例

class Parent{
	public Parent(){}//構造方法
	final int getPI(){
		return Math.PI;//終結方法
	}
}

說明:

getPI是用final修飾符宣告的終結方法,不能在子類中對該方法進行覆蓋,因而如下宣告是錯誤的。

Class Child extends Parent{
	public Child(){}
	int getPI{
		return 3.14;//錯誤:不允許覆蓋超類中的終結方法
	}
}

綜上終結類是隻能直接使用不能被繼承的類,終結方法是隻可以原樣使用不可以被覆蓋的方法。


3.4 抽象類

抽象類往往代表比較抽象的概念,在抽象類中通常要規定整個類家族各級子類都必須具有的屬性和方法。

抽象類

  1. 類名前加修飾符abstract
  2. 可包含常規類能包含的任何成員,包括非抽象方法。
  3. 也可包含抽象方法:用abstract修飾,只有方法原型,沒有方法體的實現。
  4. 沒有具體例項物件的類,不能使用new方法進行例項化,只能用作超類。
  5. 只有當子類實現了抽象類中的所有抽象方法,子類才不是抽象類,才能產生例項。
  6. 如果子類中仍有抽象方法未實現,則子類也只能是抽象類,那就只能寄希望於下級子類實現抽象方法以產生例項。

抽象類宣告的語法形式

abstract class Number{
...
}

如果寫:

new Number();

編譯器將會顯示錯誤。

抽象方法

抽象方法規定了一種行為的訪問介面,通常是在抽象類中規定子類都必須要有這樣的行為,但是行為的具體實現沒有給出。

  1. 抽象方法宣告的語法形式:

    public abstract <returnType><methodName>(...)
    
  2. 僅有方法原型,而沒有方法體。

  3. 抽象方法的具體實現由子類在它們各自的類宣告中完成。

  4. 只有抽象類可以包含抽象方法,即,若打算在方法中寫抽象方法,則定義類為抽象類。

抽象方法的優點

  1. 可以同以用這種方式設計整個類家族的對外公共服務介面。
  2. 隱藏具體的細節資訊,所有子類使用的都是相同的方法原型,期中包含了呼叫該方法時需要了解的全部資訊。
  3. 強迫子類完成指定的行為,規定所有子類的標準行為。

例:抽象的繪圖類和抽象方法

各種圖形都需要實現繪圖方法,可在它們的抽象超類中宣告一個draw抽象方法。

abstract class GraphicObject{
    int x,y;
    void moveTo(int newX,int newY){
        ...
    }
    abstract void draw();
}

然後在每一個子類中重寫draw方法,例如:

class Circle extends GraphicObject{
    void draw(){
        ...
    }
}
class Rectangle extends GraphicObject{
    void draw(){
        ...
    }
}

3.5 泛型

泛型的本質是將型別引數化。有泛型的類、泛型的方法和泛型的介面。

例:泛型類

class GeneralType <Type>{
    Type object;
    public GeneralType(Type object){
        this.object = object;
    }
    public Type getObj(){
        return object;
    }
}
public class Test {
	public static void main(String[] args) {
		GeneralType<Integer> i = new GeneralType<Integer>(2);
		GeneralType<Double> d = new GeneralType<Double>(0.33);
		System.out.println("i.object = " + (Integer) i.getObj());
		System.out.println("i.object = " + (Integer) d.getObj());//編譯錯誤:Cannot cast from Double to Integer
	}
}

例:泛型方法

class GeneralMethod {
	<Type> void printClassName(Type object) {
		System.out.println(object.getClass().getName());
	}
}

public class Test {
	public static void main(String[] args) {
		GeneralMethod gm = new GeneralMethod();
		gm.printClassName("hello");
		gm.printClassName(3);
		gm.printClassName(3.0f);
		gm.printClassName(3.0);
	}
}

例:使用萬用字元

會進一步加強程式的通用性

class GeneralType <Type>{
    Type object;
    public GeneralType(Type object){
        this.object = object;
    }
    public Type getObj(){
        return object;
    }
}
class ShowType {
	public void show(GeneralType<?> o) {
		System.out.println(o.getObj().getClass().getName());
	}
}
public class Test {
	public static void main(String[] args) {
		ShowType st = new ShowType();
		GeneralType<Integer> i = new GeneralType<Integer>(2);
		GeneralType<String> s = new GeneralType<String>("Hello");
		st.show(i);
		st.show(s);
	}
}

輸出

java.lang.Integer
java.lang.String

有限制的泛型

在引數Type後使用extends關鍵字並加上類名或介面名,表明引數所代表的型別必須是該類的子類或者實現了該介面。

注意,對於實現了某介面的有限制泛型,也是使用extends關鍵字,而不是implements關鍵字。

例:有限制的泛型

class GeneralType<Type extends Number> {
	Type object;

	public GeneralType(Type object) {
		this.object = object;
	}

	public Type getObj() {
		return object;
	}
}
public class Test {
	public static void main(String[] args) {
		GeneralType<Integer> i = new GeneralType<Integer>(2);
		//GeneralType<String> s = new GeneralType<String>("Hello");
        //非法,T只能是Number或者Number的子類
	}
}

3.6 類的組合

物件導向的程式是用軟體來模擬現實世界中的物件,而現實世界中的物件往往是由部件組裝而成的。

類的組合也是一種類的重用機制,表達的是包含關係,”有一個“的關係。

組合的語法

將以存在的類的物件放到新類即可

例如,可以說:廚房kitchen中有一個爐子cooker和一個冰箱refrigerator。所以,可以簡單地把物件myCookermyRefrigerator放在類kitchen中。

class Cooker{//類語句}
class Refrigerator{//類語句}
class Kitchen {
	Cooker myCooker;
	Refrigerator myRefrigerator;
}

例:組合舉例——線段類

一條線段包含兩個端點

public class Point {
	private int x, y;// coordinate

	public Point(int x, int y) {
		this.x = x;
		this.y = y;
	}

	public int GetX() {
		return x;
	}

	public int GetY() {
		return y;
	}
}
class Line {
	private Point p1, p2;

	Line(Point a, Point b) {
		p1 = new Point(a.GetX(), a.GetY());
		p2 = new Point(b.GetX(), b.GetY());
	}

	public double Length() {
		return Math.sqrt(Math.pow(p2.GetX() - p1.GetX(), 2) + Math.pow(p2.GetY() - p1.GetY(), 2));
	}
}

繼承和組合的異同

繼承表達的是”是一個“”是一種“的從屬關係;組合表達的是”有一個”的包含關係。在宣告覆雜的類的時候往往繼承和組合都用得上。

例:組合與繼承的結合

class Plate {// 宣告盤子
	public Plate(int i) {
		System.out.println("Plate constructor");
	}
}

class DinnerPlate extends Plate {// 宣告餐盤為盤子的子類
	public DinnerPlate(int i) {
		super(i);
		System.out.println("DinnerPlate constructor");
	}
}

class Utensil {// 宣告器具
	Utensil(int i) {
		System.out.println("Utensil constructor");
	}
}

class Spoon extends Utensil {// 宣告勺子為器具的子類
	public Spoon(int i) {
		super(i);
		System.out.println("Spoon constructor");
	}
}

class Fork extends Utensil {// 宣告餐叉為器具的子類
	public Fork(int i) {
		super(i);
		System.out.println("Fork constructor");
	}
}

class Knife extends Utensil {// 宣告餐刀為器具的子類
	public Knife(int i) {
		super(i);
		System.out.println("Knife constructor");
	}
}

class Custom {// 宣告做某事的習慣
	public Custom(int i) {
		System.out.println("Custom constructor");
	}
}

public class PlaceSetting extends Custom {// 餐桌的佈置
	Spoon sp;
	Fork frk;
	Knife kn;
	DinnerPlate pl;

	public PlaceSetting(int i) {
		super(i + 1);
		sp = new Spoon(i + 2);
		frk = new Fork(i + 3);
		kn = new Knife(i + 4);
		pl = new DinnerPlate(i + 5);
		System.out.println("PlaceSetting constructor");
	}

	public static void main(String[] args) {
		PlaceSetting x = new PlaceSetting(9);
	}
}

輸出

Custom constructor
Utensil constructor
Spoon constructor
Utensil constructor
Fork constructor
Utensil constructor
Knife constructor
Plate constructor
DinnerPlate constructor
PlaceSetting constructor

由上可知,首先呼叫PlaceSetting超類構造方法,然後構建物件成員:先呼叫Spoon超類構造方法,Utensil,同樣的次序構造Fork Knife Plate ;呼叫DinnerPlate超類Plate構造方法再呼叫DinnerPlate構造方法。


3.7 小結

  1. 介紹了Java語言類的重用機制,形式可以是繼承組合。繼承表達了一種從屬關係,“是一種”“是一個”;組合表達了一種包含關係,是一種部件組裝的思想,兩者都實現了類的重用,使我們可以在已有類的基礎上設計新的類,提高了軟體開發的效率,並且程式的可靠性和穩定性會更好。

  2. Object類的主要方法。Object類是Java中所有類的直接或間接的超類,在整個類繼承體系的最上端。因此,在Object類中規定了所有類都必須具有的屬性和行為。但從Object類中直接繼承的行為和功能不一定好用,需要我們自己去覆蓋,如equals等方法,但是終結方法、靜態方法等不能覆蓋。

  3. 終結類和終結方法的特點和語法。

  4. 抽象類和抽象方法的特點和語法。無法實現方法體,在超類中宣告抽象方法。抽象方法的具體實現留給子類或子類的子類直至某一級子類