1. 程式人生 > 其它 >Java學習記錄:2022年1月13日(其二)

Java學習記錄:2022年1月13日(其二)

Java學習記錄:2022年1月13日(其二)

摘要:本篇筆記主要記錄了在設計類時的一些注意事項,類載入時類中各個部分的載入順序以及繼承和多型的知識。

@

目錄

1.域初始化

​ 域初始化就是一個物件的屬性的初始化,我們在13日的筆記(其一)中得知了屬性的宣告以及初始化的區別,當一個屬性被人為的賦值時,才被系統認定為初始化,因此一個物件型別變數的初始化不代表它裡邊的屬性被初始化了。如下:

class Cat{
	private int a;
    private int b;
    private int c;
}

​ 這個類被聲明瞭三個域,分別為a、b、c,他們在被宣告的時候沒有被賦值,實際上在宣告類的時候賦值並不是一個特別好的設計方式,因為類本身就是作為一個抽象概念存在的,它裡邊的屬性列表只是標定了這一類的物件都有什麼屬性,而非是要標定這個類中的屬性都具體是什麼值,畢竟每一個人類雖然有相同的屬性,但是都具備足以區分彼此的屬性值。

​ 因此當我們根據這個類例項化一個物件的時候,如下:

Cat cat1 = new Cat();

​ 這個cat1變數確實是初始化了,但是cat1物件的屬性:a、b、c沒有被初始化,它們儘管有一個基於安全被系統賦予的初值,但是系統認定這個初值除了保障安全以外沒有其他意義,認定這個域是沒有被初始化過的,因此我們在宣告一個物件型別變數後,需要對這個物件的域進行初始化。怎麼進行初始化呢?大部分情況下我們使用物件.屬性名 = XXX

的方式初始化就好了,但在這裡不行,因為這個類的屬性都是private型別的,都是私有屬性,我們無法直接在物件的外部呼叫它的屬性,因此我們在這個時候通常會使用修改器,如果要使用修改器,就必須這樣定義這個類:

class Cat{
	private int a;
    private int b;
    private int c;
    public void setA(int a) {
		this.a = a;
	}
    public void setB(int b) {
		this.b = b;
	}
    public void setC(int c) {
		this.c = c;
	}
}

​ 然後在宣告並初始化之後這樣使用:

Cat cat1 = new Cat();
cat1.setA(123);
cat1.setB(456);
cat1.setC(789);

​ 這種寫法著實有點麻煩,當然不是說使用修改器是一件麻煩事,修改器是一個很有用處的東西,但是使用修改器為一個物件的域進行賦初值,實在是一件不太簡明的寫法,當一個類的私有屬性非常多的時候,使用修改器進行初值賦予會導致程式碼非常的冗餘,且當你漏寫一個的時候,系統並不會報錯,這時我們選擇構造器,使用構造器進行域初始化是一件再好不過的事情了。如下:

class Cat{
	private int a;
    private int b;
    private int c;
    public Cat(int a, int b, int c){
        this.a = a;
        this.b = b;
        this.c = c;
    }
    public void setA(int a) {
		this.a = a;
	}
    public void setB(int b) {
		this.b = b;
	}
    public void setC(int c) {
		this.c = c;
	}
}

​ 當我們書寫了這樣一個構造器的時候,在進行類例項化的時候,就不得不這樣書寫:

Cat cat1 = new Cat(123,456,789);

​ 否則就會報錯,這是因為類中自帶的構造器的許可權強度非常弱,系統認為它只是一個用於佔位的結構,而非一個有實際價值的構造器,當虛擬機器系統在編譯的時候檢測到使用者書寫的構造器之後,就會認定但凡是使用者寫的,那一定是有用的,而既然是有用的,那用處一定是大於預設構造器的,因為預設構造器是一點用也沒有,這時使用者書寫的構造器就會完全替換預設構造器,預設的無參構造器便完全不能被呼叫了。因此當我們這樣設計構造器的時候,如果使用的是稍微高階一點的IDE,只要屬性漏寫之後,它一定會標紅,我們就能意識到:“奧,少寫東西了。”與此同時,使用構造器這樣進行賦初值,也是一個非常簡單明瞭的寫法,比使用修改器方便了很多。當一個物件在被宣告出來的時候就有自身的含義時,最好使用構造器對其進行賦初值,這樣對於整個程式的安全性都有很大的意義

2.構造器的詳解

​ 之前我們提到過構造器的作用,實際上不是構造物件實體,而是進行一個物件實體的初始化,同時也簡單學習過構造器的一些基礎知識,比如構造器的特點為:

1.構造器與類同名

2.每個類可以有一個以上的構造器

3.構造器可以有0個、1個或者多個引數

4.構造器沒有返回值,自然在定義時也沒有返回值型別

5.構造器總是伴隨著new一起使用

​ 而對於構造器的一些其他用法,我們並沒有深入的探討過,現在我們對構造器的一些具體用法進行深入探討,需要注意的是:構造器的所有用法都和上面列出的五條特性相關

1.任何一個類中存在一個預設構造器

​ 在之前的筆記中提到過,預設構造的存在的作用是為了體現Java中類例項化的機制特性而預設存在的,它的存在只是告訴大家:使用Java語言,例項化一個物件,我們可以使用構造器這一機制來初始化一下這個例項。基於這個機制的重要性以及底層設計的簡約行,Java規定無論如何必須使用構造器,不使用構造器初始化一下的話就會報錯,但有時大家確實用不到構造器,因此在Java語言中設計了一個預設的無參構造器,如果使用者不定義自己的構造器時,就需要使用這個構造器佔位,保證虛擬機器編譯時不會檢測出錯誤,這個無參構造器什麼也不做,它內部是空的,它只起到一個佔位的作用,並且提示著大家:Java類例項化支援使用構造器對物件實力進行初始化

2.使用者自定義構造器與預設構造器

​ 構造器本質上是方法的一種,眾所周知在Java中支援方法重寫,只要方法簽名不一樣,那方法都可以用,但是:使用者在自己寫一個構造器後,預設的構造器就不能用了。這是因為預設構造器被系統認定為:僅用於佔位的,無其他作用的構造器。當用戶自定義一個構造器,那使用者肯定是有自己的考量,肯定是有額外的需求,肯定是需要一個有用的構造器,如果不是這樣,那使用者為何要費力自己寫一個呢?這時肯定是預設構造器不滿足使用者需求,使用者才定義的,基於這種思想,Java被設計成:一旦使用者自定義一個構造器,預設構造器便自動隱退。因為它什麼用也沒有,使用者自己寫的構造器也可以佔位,可以讓系統不報錯,不僅如此,它還有其他的更多的功能,本著程式碼簡約的原則,這個預設構造器就沒有存在的必要了,因此當一個類中存在使用者自己定義的構造器時,預設構造器自動失效,不再被可被呼叫。然而規矩是死的人是活的,有些人在自己定義了構造器之後,仍然想使用之前的無參構造器,這時我們雖然不能使用預設的無參構造器,但是可以自己寫一個無參構造器,如下:

class Cat{
	private int a;
    private int b;
    private int c;
    public Cat(int a, int b, int c){
        this.a = a;
        this.b = b;
        this.c = c;
    }
    public Cat(){
        
    }//再自己定義一個無參構造器就行
    public void setA(int a) {
		this.a = a;
	}
    public void setB(int b) {
		this.b = b;
	}
    public void setC(int c) {
		this.c = c;
	}
}
3.類中可以有多個構造器

​ 這實際上是基於構造器的的本質實際上是方法,它是一種特殊的方法。在Java的類中,方法名不是區分方法標識,方法簽名才是,方法簽名包含:方法名,形參列表。因此只要形參列表不同,即使構造器們的方法名都必須是類名,也無所謂,可以同時存在多個。

4.構造器可以有任意的引數

​ 構造器的引數列表沒有嚴格的限制,通常來說構造器多被用於進行物件屬性的賦初值,不同的引數列表是針對不同情況的賦初值的,如果有需求的話,自己定義一個無參構造器也是可以的,這個並沒有嚴格的定義,複合設計結構和我們的需求即可。

5.構造器的巢狀呼叫

​ 在構造器中我們可以呼叫其他構造器,因為構造器實際上就是一系列的邏輯程式碼,是一系列的操作,能在一個方法中呼叫另一個方法實際上是很正常的事情,關於構造器的呼叫有兩點注意:只有構造器能呼叫構造器,其他方法不能呼叫構造器;在構造器呼叫構造器時,必須位於第一行。如下圖所示:

​ 我們在構造器內部呼叫一個構造器時,呼叫方式是直接使用關鍵詞this進行呼叫,而非使用方法名,否則會報錯,上圖是一個正確的呼叫,現在我們來看看各種不正確的呼叫方式:

​ 上面圖中這三種情況基本上代表了所有的錯誤情況,總而言之,關於構造器內呼叫構造器的方式,是直接使用this關鍵字呼叫,這個實際上就是認為設計導致的,因為this在類中可以表示被例項化的類的某個物件本身,而構造器實際上也是屬於一個物件的,因此就使用this直接表示它自己的構造器了,這樣簡單易懂,可以節省不少程式碼量。在類的內部,實際上是以this代表自身生成的物件的,使用this的邏輯程式碼實際上就是在生成物件或者進行操作時對於本身的呼叫。而構造器實際上也是屬於物件的,因此this也被設計成這個物件的構造器的呼叫方式。這裡我們簡單記為構造器的呼叫方式為this(引數列表)即可。

​ 那麼構造器為什麼必須在構造器中呼叫,且構造器只能在第一行被進行呼叫呢?這是因為構造器不是普通的方法,它被加上了限制,構造器被要求必須在這個例項的實體被構造完成之後,地址返回之前進行執行,它完成的是這個物件中一些資訊的初始化。也就是說,它必須在一個物件中的各種資訊被初始化之前被呼叫。因此,如果這個構造器不被書寫在第一行,在它之前就有可能存在對整個物件中的域有意義的修改,如在前面對一個域進行了賦值,而在進行有意義的賦值之後,再呼叫其他的構造器,其他的構造器中也難免存在對同一屬性進行的賦值操作,這樣一來,就會導致之前的賦值被破壞,進而導致最終得到的一個錯誤賦值。而在其他普通方法中則更不能輕易呼叫了,一來是普通方法在被呼叫的時候,這個物件已經建立好了,這時系統中已經不允許這個物件再使用構造器了,其次,即使系統允許,構造器在這裡也會破壞這個物件的資料,導致資料錯誤,因此基於安全考慮,這種情況也不被允許。

總體上構造器必須在第一行是基於安全考慮,防止它破壞已有的有意義的屬性賦值,至於在上面第四張圖中,為什麼在呼叫構造器的前幾行沒有對屬性的賦值,它也會報錯呢?這是因為虛擬機器沒有那麼智慧,它並不能完全確定某一次賦值行為是都是為屬性賦值,因此基於絕對的安全性考慮,構造器在構造器中的呼叫被規定在了第一行。只要在第一行,不管怎樣,它都不會破壞我們自己書寫的已有的賦值了,這是絕對安全的。因此,構造器在構造器中的呼叫必須被書寫在第一行

在構造器中呼叫構造器時,之所以必須在第一行呼叫,是因為是考慮到如果前面的行存在關於屬性的賦值行為,構造器的呼叫可能會破壞已經賦值好的資料。是基於安全考慮的。

3.類的初始化(面試重點)

​ 類在剛剛進入整個虛擬機器中準備被進行使用和編譯的時候,是需要進行初始化的,這個過程我們稱之為類初始化,類初始化和類例項化不是一碼事,類初始化是類在進入虛擬機器是被載入到方法區中時的這個過程,我們也稱這個過程為類載入的過程。類載入過程在它第一次被例項化為一個物件的時候執行

​ 在類初始化的整個過程中,類中的所有部分都會被載入,有些部分還會執行,接下來我們介紹類在初始化的時候各個部分被執行的過程。首先我們需要知道的是類中一般都有哪些組成部分以及哪幾類部分。

​ 在類中,主要分為:靜態塊,普通塊,以及一個特殊的部分:構造方法。在一個類中,靜態塊永遠是優先被執行的,普通塊其次,而在整個過程中,構造方法是最後被執行的,接下來我們使用程式碼瞭解這一過程:

package www;

public class Car{
	private static String name;
	private String gender;
	public Car(){
		
		System.out.println("我是一個構造方法");
	}
	{
		int a = 10;
		System.out.println("我是一個普通塊");
	}
	
	static {
		int a = 10;
		System.out.println("我是一個靜態塊");
	}
}

​ 這個程式碼的輸出過程為:

我是一個靜態塊
我是一個普通塊
我是一個構造方法

​ 也就是說,在一個類中,當這個類被載入的時候,靜態塊是先被執行的,普通塊是在靜態塊之後被執行的,而構造方法是最後被執行的。我們知道,類載入的過程實際上是這個類被載入到方法區的過程,在這個過程中,類中的靜態區域會被優先賦予一次記憶體,它們的記憶體會被賦予到靜態資源區上,這個過程是優先執行的,因此靜態的區域是最先被執行的。而普通塊的執行,則是在為這個物件記性地址分配的時候進行的,這個步驟是在堆記憶體上記性的,它是晚於在方法區被分配記憶體的,因此它自然地也比靜態塊執行的時間晚。那構造方法為什麼會在最後被執行呢?這是因為構造方法是在整個記憶體空間被分配好之後才被執行的,它是最後的善後人員,因此它是最後被執行的。

​ 所以在一個類進行第一次物件宣告的時候,內部的區域執行的順序是:靜態塊(包括靜態域和靜態塊以及靜態方法,他們之間的先後載入由書寫的先後確定,其中靜態方法不會被執行,它作為一個方法僅在被呼叫的時候執行,被執行的是靜態塊,但是他們是同時間被載入到記憶體上的)--------->普通塊(就是不被static修飾的各種域,塊以及方法,他們實際上是在被分配到堆空間上時執行的)--------->構造器(構造器是在一個物件實體建立好後被執行的,也就是說它在普通塊的空間被分配好之後執行,這就是它最後被執行的原因。)

​ 當這個類存在繼承其他類的情況時,如下:

package com.ysmw;

public class Car {
    public int speed;
    public int number;
    public int type;
    public Car(){
        System.out.println("我是一個Car類的構造方法");
    }

    {
        System.out.println("我是一個Car類的普通塊");
    }
    static{
        System.out.println("我是一個Car類的靜態塊");
    }
}
package com.ysmw;

public class Truck extends Car {
    public Truck(){
        System.out.println("我是一個Truck類的構造方法");
    }

    {
        System.out.println("我是一個Truck類的普通塊");
    }
    static{
        System.out.println("我是一個Truck類的靜態塊");
    }
}

執行結果為:

我是一個Car類的靜態塊
我是一個Truck類的靜態塊
我是一個Car類的普通塊
我是一個Car類的構造方法
我是一個Truck類的普通塊
我是一個Truck類的構造方法

​ 也就是說,當一個類繼承了其他類作為父類的時候,在它被例項化的過程中,類載入的步驟為:父類中的靜態塊--->子類中的靜態塊---->父類中的普通塊----->父類中的構造方法---->子類中的普通塊----->子類中的構造方法

​ 我們發現:當存在繼承關係的時候,靜態塊的執行肯定是優先的第一順位,也就是說,子類中的塊也好,父類中的塊也好,肯定是靜態區域的優先被執行;第二順位是父類優先,從上面的執行結果來看,儘管父類和子類中的靜態塊都要優先於其他部分執行,父類的靜態塊仍然優先執行。

​ 第二順位是父類優先原則還導致了在靜態塊優先執行完之後,就是父類中的其他部分優先執行,即使是父類中的構造器,也要優先於子類中的構造器,在父類的其他部分按照規律執行完畢之後,才輪到子類中的其他塊。

​ 我們記為:靜態優先為第一順位,父類優先為第二順位,普通塊較構造器的執行並不體現在第二順位中,子類除靜態塊以外的部分,需要等父類的除靜態塊以外的部分全部執行完之後才會執行。

​ 實際上這是因為Java執行時在進行類載入以及類例項化時的機制:首先檢測到了Truck類使用了new語句,這時會檢索Truck的路徑找到這個類,如果這個類沒有被載入過,那麼這時就要對這個類進行方法區上的載入,當對這個類的資訊進行掃描時,首先發現了這個類裡邊繼承了Car類,這時系統便會順著路徑先載入Truck類繼承的Car類,然後就開始對Car進行載入行為,對Car進行載入的時候發現Car中存在靜態標識的塊,這時就會為這個塊分配靜態資源區上的空間,被分配空間後的邏輯程式碼就會被執行,因此在這個時候,Car類中的靜態塊首先被執行,之後Car類被載入好之後,執行時系統會退回Truck類的操作,繼續載入Truck類,這時執行時發現Truck也有static修飾的語句塊,這時執行時也會為這些靜態語句塊分配靜態資源區的記憶體,因此這時這些語句也會被執行。在載入完子類之後,類載入過程就宣告結束了,之後會進行物件例項構造的過程,在物件例項構造的過程中,是要為這個類進行例項化,這時物件中的其他部分會被分配堆空間上的記憶體,因此普通程式碼塊的建立執行在這個時候才會發生,而即使是在建立物件時,也是先建立一個父類物件,然後才建立子類物件,因此這時會先執行父類中的其他塊區,然後執行完父類中的其他塊區後,再執行父類相應的建構函式,而這個建構函式會將父類物件的地址返回到子類例項中的類資訊中,這個資訊在子類中通過super可以查詢到,也就是說在父類物件完全建立完畢之後,才會繼續建立子類物件,之後子類物件會執行自己的普通區塊,並在物件完成記憶體分配之後,執行構造器返回自己物件例項的地址。

​ 因此我們知道了:靜態部分是在類的載入中進行,其他部分都是在類的初始化中進行,而類的載入僅在類第一次被例項化的時候進行一次,因此當我們重複的new物件的時候,會發現靜態塊只執行了一次之後便不再執行了,且靜態部分永遠是在最前邊,因為靜態部分的執行和其他部分的執行完全不是在同一個記憶體,不是在同一個過程中的。

4.在構造方法中呼叫超類的構造方法

​ 超類實際上就是父類,在子類中,使用super呼叫父類物件中的東西,同時,在構造器中可以使用super()來呼叫父類的構造方法,同時,當我們在構造器中呼叫父類的構造方法的時候,父類的構造方法也要出現在第一行,如:

package com.ysmw;

public class Truck extends Car {
    public Truck(){
        super();
        System.out.println("我是一個Truck類的構造方法");
    }

    {
        System.out.println("我是一個Truck類的普通塊");
    }
    static{
        System.out.println("我是一個Truck類的靜態塊");
    }
}

​ 否則就會報錯:

​ 關於這裡的原理我並不是特別清楚,在之後的筆記中會有相關的解答,目前我們先記住:在子類構造器中呼叫父類構造器時,使用super()進行呼叫,它和this一樣必須位於第一行,且它和this()不能共存

5.靜態匯入

​ 在Java中有著普通的匯入方式import,還存在另外一種匯入方式:靜態匯入。靜態匯入的書寫方式為import static,如下:

import static java.util.*;

​ 靜態匯入和普通匯入的區別在於,靜態匯入是在整個程式執行之初,就將匯入目標直接載入進記憶體中,虛擬機器使用的時候,直接從記憶體中獲取類資訊並進行類載入,而非從硬碟中請求,這樣一來會有效的加速程式執行速度,但是會導致記憶體高佔用的情況。

6.繼承

1.關於在子類構造器中呼叫父類構造器的問題

​ 在Java中存在一種概念:繼承,繼承指的是一個類可以像繼承基因一樣繼承另外一個類的所有公共方法,公共屬性。基本上除去private,屬性都可以繼承,沒有修飾符修飾的屬性,當跨包繼承時不會繼承來。關於各種修飾符的許可權,這裡有一個不錯的文章。Java中不存在直接的多繼承,一個類只能直接繼承一個父類。繼承方式是使用extends關鍵詞來繼承,如下:

package com.ysmw;

public class Truck extends Car {
    public Truck(){

        System.out.println("我是一個Truck類的構造方法");
    }

    {
        System.out.println("我是一個Truck類的普通塊");
    }
    static{
        System.out.println("我是一個Truck類的靜態塊");
    }
}

​ 在繼承關係中,父類也被成為超類,子類也被稱為孩子類,派生類。根據修飾符的訪問許可權,子類了可以繼承到當下一切可以訪問到的方法,子類中可以書寫這個方法的同名方法進行覆寫。覆寫指的是子類中書寫了一個和父類中同名的方法之後,這個方法就會覆蓋父類中的方法,作為子類獨特的方法存在,我們使用子類物件直接呼叫到的這個名字的方法的方法邏輯,就是在子類中書寫的,完成的功能也是子類方法所對應的功能。當然我們可以通過super這個關鍵字對父類物件進行訪問,使用這個關鍵字我們就可以在子類中呼叫父類,進而呼叫父類的方法影響父類內部的屬性值,在構造出一個新的子類物件時,會先構造一個沒有名字的父類物件,這個父類物件不能直接被呼叫,使用在子類中使用super關鍵字可以進行直接的呼叫。

​ 在上面我們曾經提到了一個知識點,那就是在子類構造器中呼叫父類構造器時,父類構造器要在第一行,這是因為如果不在第一行,子類構造器中的程式碼會優先於父類構造器執行,這是不符合初始化順序的,因此我們必須將父類構造器放置到第一行,讓它在子類構造器執行之前進行執行。這裡實際上涉及一個底層知識,父類的構造器即使被放置在子類構造器中,也會優先於子類中的普通塊以及構造器執行,但是當它不是位於第一行時,這個機制就被破壞了,因為在它之前有子類構造器的程式碼,子類構造器就已經開始執行了,這樣就會直接違背初始化順序,因此會報錯。我們可以記為:父類構造器必須優先於子類構造器執行,因此在子類構造器中呼叫父類構造器的時候,必須在子類構造器程式碼執行之前進行呼叫。這裡是一個面試點,需要我們注意。

​ 更加需要注意的是,當父類的構造器需要傳參的時候,在子類中必須重寫構造器並在子類構造器中呼叫這個含參的父類構造器,這樣才能為父類物件初始化的時候傳參,這也是一個面試點,需要我們注意。這裡的原理實際上是因為如果子類不在構造器的第一行書寫父類構造器,編譯器也會在這裡加上一個無參構造器,實際上父類的構造器就是在這裡的,在程式執行的時候系統會檢測子類構造器中的這個位置,並將檢測到的父類構造器呼叫進行執行,也就是說這裡無論如何會被加上一個父類構造器的呼叫,當這裡不寫this()或者super()時,在編譯的時候系統也會在這裡加上父類的預設構造器,因此當父類構造器需要傳參的時候,這裡必須寫父類構造器中的含參構造器。關於這個問題,我進行了更加詳細的解讀,詳情請點選連結

2.關於繼承的詳解

​ 一個類只能有一個父類,但是可以有很多孩子類。每次繼承,只能在extends後邊寫一個繼承類名。或者說,對於向上的繼承,類不能同時繼承多個類。繼承是一個樹狀的結構,因為一個類只能直接繼承一個父類,不能同時繼承多個父類,不能夠多繼承,但是實際上可以間接的繼承多個類。一個類也可以被多個類繼承。由於這種繼承樹的存在,一個類可能有很多子孫類,有間接的有直接的,越往下,這些類加入的自己的東西就越多,可以說越往下的越新。因此基於木桶原則,Java中引出了多型的概念。

​ 多型就是多種形態的意思,也就是說父類可以表現為任一子類的形態,或者說父類型別的控制代碼可以承受子類型別的物件實體值。為什麼說是木桶原則呢,因為父類中沒有的屬性,子類中可能有,因此一個子類的建立好的例項可以被塞給一個父類的控制代碼,因為根據父類的類資訊,在這個實體中可以找到父類有的所有東西,因此係統不會報錯,簡而言之就是隻要父類有的子類都有,因此父類沒有的部分可以被遮蔽,被遮蔽之後,這個子類就會被偽裝成這個父類的樣子,這就叫多型。而子類的控制代碼不能承接父類的,因為子類中有一些東西父類是沒有的,因此當我們把一個父類的實體賦予給一個子類的控制代碼,使用子類控制代碼進行操作時就會出現各種空指標錯誤,因此這種反過來的賦值是行不通的,多型只能指:將子類的值賦予給父類的值。

​ 實際上就是向上相容的原理,父類好比是一個能裝1升水的小木桶,子類是一個能裝5升水的大木桶。子類可以通過鋸掉上邊的部分變為父類,但父類卻不能通過增加木頭變成子類,這便是多型的原理,子類物件可以通過父類控制代碼的訪問規則遮蔽掉多出來的欄位,而父類物件中沒有的東西,使用子類的訪問規則就會出現空指標異常。因此父類控制代碼 = 子類物件,將一個子類物件轉變為父類型別的行為被稱為多型。因此,使用多型時,超類只能呼叫自己確實宣告過的方法,子類自己創新出來的新方法超類用不了。

​ 當我們將一個子類物件變為父類物件的時候,可以使用強制型別轉化再轉化回來,或者轉化成自己的其他父類,這個強制型別轉化的過程實際上就是恢復被遮蔽掉的部分的行為,當然這個過程也充滿了各種複雜情況,如下:

宣告一個類A
B繼承了A,C繼承了B,D繼承了C
E繼承了A,F繼承了E

​ 這樣一來就有如圖所示的繼承樹:

​ 這時我們就可以做如下的操作:

A a = new D();//將D型別的例項存放進A型別的變數中
a = (D)a;//將a控制代碼強制轉化為D型別

​ 當我們對a進行強制轉化時,還可以轉化為自己的其他父類,比如C類,B類,但是注意E類和F類轉化不了,因為E類和F類不是D類同一個路徑上的父親,可以說不是直系親屬,這會導致E和F中可能存在D中不存在的欄位,因此不能將D強制轉化為E和F在執行的時候會報錯,可能會出現嚴重的空指標錯誤,難受的是IDE不會喂為你自動報錯,它檢測不出強制型別轉化時的問題,因此我們在使用強制型別轉化的時候,需要使用到一個方法:instanceof,這個方法用於判斷前邊的物件是否是後邊的子孫後代類的物件。使用方法為:

d.instanceof(A);//檢測變數d的型別是否為A型別以及是否為A的子孫型別,在進行強制轉化之前先使用這個方法進行檢測,更加安全。

​ 如上為13日的基本內容,下面我附上筆記原文。

7.筆記原文

​ 需要注意的是筆記原文僅供參考,且筆記原文中存在一些錯誤以及之前不懂得知識點,在上邊的筆記中已經對裡邊的疑問進行了全面的解答,因此在檢視筆記原文時如果發現有地方是錯的或者說進行了標註,說暫時不明白,請直接忽略。

	整個類組成
	類的屬性也叫域
	構造方法沒有返回型別,與類同名
	方法同名,入參不同,系統就可以區分,這叫方法的過載
	在new物件的時候,new後邊呼叫的時候就會呼叫這個構造方法,對於有參的構造方法,並不會完全覆蓋無參的構造方法,都可以用
	類中還有一個東西叫註解,也是java類資訊的一部分
	識別方法只看方法名和入參,不看返回型別,因此方法名和入參一樣,返回型別不一樣,仍然會報錯,注意
	只要方法名和入參一樣,就會認為是同一個方法
	this表示的是隱含當前物件的意思,也就是在物件裡邊使用自己的域時,為了和外部引數的同名變數區分,通常使用this來表示是自己的變數的意思。我們可以理解為,當在一個類的內部對自己使用某方法或者呼叫自己的域時,用this
	在每一個方法中,this表示隱式引數,也就是說在每一個方法中,表示自己的隱式引數是this
	final為什麼不可修飾引用型別?final只對被他修飾的控制代碼阻止賦值,它不允許整體賦值,但是可以對裡邊進行修改,也就是說對於堆地址它並管不到,會導致不可控。final對於引用型別是防止改變指向,防禦不了引用型別在本地址上修改,但是它可以修飾字符串,因為字串不可變,在本地址上改不了,因此可以修飾字符串。final修飾的引用型別不能改變控制代碼的指向,但是放置不了指向地址上的修改。
	靜態的常量在一開始必須賦值,在域中的的常量不一定賦值,但是在類載入的時候必須執行。
	final修飾的變數要求它存在的時候就要有值,靜態的上來就要賦值,因為靜態的在進入記憶體的時候就已經存在了。
	程式碼塊在類載入的時候會執行,什麼是類載入?就是載入進方法區的時候,原來如此,那麼我上邊理解錯了,final修飾的在類載入的時候賦值就行了。
	如果涉及到公共的,static修飾的變數,它會向全程式公開,因此這時我們會用final將它修飾成常量,讓其全程式公開且不可變
	這樣的常量可以直接用類進行呼叫,只要在一個包下,就可以用類加常量名進行呼叫
	跨包呼叫一般來說就不能直接訪問了,public是包內訪問,要想跨包,就得先導包。
	靜態方法就是無需使用例項,直接使用類,就可以直接呼叫,因為它隨著類已經載入進記憶體裡了,它已經存在了
    設計模式需要大量的程式碼經驗,常見的是23種,是重要的面試點!
	如果傳的是堆中地址,傳的是值叫值傳遞,傳的是棧中地址叫引用傳遞,棧地址叫引用地址,也就是說基本型別傳的是棧地址,是引用傳遞
	我們可以簡單記住引用傳遞傳的是棧中的地址,然後將地址中的資訊拷貝進入參
	值傳遞是傳遞值,引用傳遞是傳遞真正的地址
	預設域初始化就是讓變數預設的有初值
	上邊我想錯了,對於引用傳遞我是學習過的,這一點絕對沒有錯,不過確實可以單純的從位置來看,我明白了,用&取地址是一個原理,關鍵在於我們是得到了源資料還是一份拷貝,引用傳遞就是拿地址,拷貝的不是值而是棧上的地址

	顯式的域初始化,就是在寫類的時候給出初值,宣告變數的時候給出初值,這樣一來,我們只要例項化一個物件,其初值都是那樣的。
	命名其實還是要按照相應的命名規則來命名,不要不規範,一定要規範一些,該大寫大寫該小寫小寫,該如何駝峰就如何駝峰,總之要符合命名規範。以後還會更加詳細的講命名規範
	我們可以在自身呼叫自身的構造方法,可以用this來呼叫,必須在第一行
	在自身的構造方法中可以呼叫自身的構造方法,這樣可以再構造一個類,或者靈活的生成需要的類和類之間的連線
	注意只能在構造方法中呼叫,且必須在第一行
	4.7.6是面試重點
	類初始化(生成物件)順序是什麼
	或者物件初始化順序
	普通方法塊優先於構造方法,靜態方法塊優先於普通方法塊,靜態的塊只執行一次,它在第一個物件處執行
	非靜態的屬性和非靜態塊屬於同一優先順序,誰在上邊誰先執行,二者是同一個優先順序。
	所有的靜態區域只由第一個物件觸發,並且只執行一次,包括靜態塊,靜態域。靜態只能由第一個物件觸發
	即使我們new一個靜態物件,這裡使用一次構造方法,它是第二個類,它也不會觸發任何靜態了,只能是它執行完之後,第一個構造方法接著來觸發後邊的靜態型別。也就是說靜態型別只能由第一個構造方法觸發
	new Person()表示生成一個物件,this()僅僅表示呼叫方法,二者還是有差別的,生成物件是可以在任意位置使用的,單純呼叫方法的話是有嚴格限制的。也就是說加上new之後就不是一碼事了,加上new之後就會返回一個物件。我們不能使用new this()的方式構建物件,只能是使用new 類名構造器的方式進行物件構造
	一個類如果繼承了一個父類,那麼在這個類初始化的時候,一定是父類先初始化,這個類才能初始化,父類中的東西先進行初始化,但是我發現了一個問題,永遠是靜態的東西優先順序更高,也就是說在父類中有靜態有非靜態,子類中有靜態有非靜態時,父類的靜態先,然後是子類中的靜態,然後是父類中的非靜態,最後再試子類中的非靜態
	同一類重複初始化時,才會體現出靜態塊只加載一次的現象,當不同的類分別初始化,他們的靜態塊都會執行
	首先是靜態優先,然後是其他其次,對於子類父類,父類限制性,子類其次,也就是說先是父類的靜態先執行,子類靜態第二,然後是父類全部非靜態優先,最後是子類
	這裡是面試大重點!一定要記住!
	這裡的順序一會再整理一遍,詳細的記住!

	在Java中例項不用之後,會自動析構
	Java類一定要存在於某個包下,包實際上就是一個資料夾
	靜態匯入是一開始就放入記憶體,這樣執行起來會非常快
	包的命名很多都是域名倒過來開頭
	包上邊的一個點代表一級目錄,如com.cloud相當於com目錄下的cloud目錄
	繼承:子類可以繼承父類一切的公共方法,包括其主方法,子類會繼承所有的內容,包括私有屬性也會繼承,好像只有保護的不被繼承,這時會新建一個沒有名字的父類例項用於儲存父類資訊,子類使用的資訊都來自於這裡,注意!
	子類不能繼承父類的私有屬性,但是如果子類中公有的方法影響到了父類私有屬性,那麼私有屬性是能夠被子類使用的。
	父類也叫超類,基類;子類也叫派生類,孩子類
	方法的覆蓋,不管父類中的方法是什麼,只要在子類中書寫了同名的方法,其就會覆蓋住父類中的方法,當呼叫子類中的這個方法時,呼叫的是子類中的新方法,這就叫覆蓋。
	重寫就是覆蓋,覆蓋就是重寫
	在子類中使用父類構造器時,父類構造器必須寫在第一行,因為在初始化時,父類構造器必須先初始化,因此必須在第一行。、
	自己呼叫自己的構造器,必須在第一行,而呼叫父類的也必須在第個行
	如果父類的構造器需要傳參,那麼子類在自己的構造方法中必須要呼叫父類的構造方法並進行傳參,鬥則會報錯。這裡也是一個面試點,注意!
    需要尤為注意的就是這一點,在父類的構造器需要傳參時,子類的構造器一定要重寫一個構造器,在裡邊呼叫父類的構造器並傳參

	一個類只能有一個父類,但是可以有很多孩子類。每次繼承,只能在extends後邊寫一個。或者說,對於向上的繼承,類不能同時繼承多個類,繼承是一個樹狀的結構。一個類只能直接繼承一個父類,不能同時繼承多個父類,不能夠多繼承,但是實際上可以間接的繼承多個類。
	多型就是多種形態的意思,父類可以表現為子類的任何一種形態,或者說父類可以承受其子類的形態
	可以用父類變數 = 子類物件,父類可以接受子類例項的賦值,進而展現出子類的樣子,這叫多型
	但是使用多型時,超類只能呼叫自己確實宣告過的方法,子類自己創新出來的新方法超類用不了


	這種方法最常用,祖宗類作為引數,他能夠接受所有的子孫類。和父類是直系親屬的子類都可以作為引數傳進去。
	總之就是父類的變數可以承擔子類,但是子類的方法父類不見得能用,這裡只能用父類有的方法。超類物件是不是隻能呼叫子類重寫的方法,子類新加的方法不能呼叫
	阻止繼承的方法是使用final字首,被final修飾的類無法被繼承,被final修飾的方法無法被重寫
	強制型別轉換隻要滿足,祖宗類的控制代碼 = 子孫後代類的物件的地址
	也就是說只能向上強制轉換,多型是向下,強制型別轉換是反過來向上
	被多型化為子類的物件可以被強制型別轉化回父類。
	instanceof判斷前邊的物件是否是後邊的子孫後代類的物件
	Object類是所有類的父類
	這裡不太明瞭,再看!
	抽象是提取共性,這個叫做抽象,也就是隻具備重要特徵
	在類中給某屬性寫死值沒有意義

	子類需要使用父類中的方法,但是具體怎麼使用不可控,這時適合使用抽象類。抽象類只定義有,不能替子類實現功能。抽象類一般都規定子類有什麼功能,但是規定不了怎麼實現。
	抽象類是做限定的,具體實現的靠子類
	抽象類側重於抽象,其子類負責具體實現
	繼承抽象類之後一定要實現抽象類的方法,否則會報錯

	父類變數可以承接子類變數,這個行為展現的是多型的性質,同時這個行為被稱為向上轉化。
	在此為前提下的情況下的父類變數可以再次轉換會子類變數,這時就需要一個強制轉化,


	在整個過程中,只允許出現父類變數 = 父類地址或者父類變數 = 子類地址的狀況
	不能出現子類變數  = 父類地址的情況
	控制代碼型別一定要是父類,控制代碼型別一定要是真實地址型別的父類才行,控制代碼型別比較容易看出來,但是真實地址的型別不見得和控制代碼一致,因為多型
	如 C繼承B,B繼承A
	A a = new C()
	a控制代碼中實際上指的是一個C型別的地址
	然後就可以有B b = new B()
	然後有 b = (b)a是這樣的

	在整個過程中,只允許出現父類型別控制代碼 = 父類型別值地址或者父類型別控制代碼 = 子類型別值地址的狀況,不能出現子類型別控制代碼  = 父類型別值地址的情況因為多型,控制代碼型別不見得和指向的地址的值的型別一致,如C繼承B,B繼承A時,有A a = new C()
	a控制代碼中實際上指的是一個C型別的地址,當存在B b = new B()時可以有:b = (b)a因為b實際上是a控制代碼指向的地址的值型別的父類
	正因為存在種紛繁複雜的情況,我們要用intenceof語句進行真相判斷