【Java】類的結構

Nemo&發表於2020-11-06

類與物件

類中主要包括五種結構,下面進行對這五種結構進行詳細的介紹。

1. 物件導向與程式導向

  • 程式導向:強調的是功能行為,以函式為最小單位,考慮怎麼做。
  • 物件導向:強調具備了功能的物件,以類/物件為最小單位,考慮誰來做。--唯物主義(物質決定意識)

2. 類與物件的關係

  • 類:對一類事物的描述,是抽象的、概念上的定義
  • 物件:是實際存在的該類事物的每個個體,因而也稱為例項(instance)

物件導向程式設計的重點是類的設計,設計類就是設計類的成員。
二者的關係:物件,是由類new出來的,派生出來的。

3. 物件導向思想實現的規則

  1. 建立類,設計類的成員
  2. 建立類的物件
  3. 通過物件.屬性 或 物件.方法呼叫物件的結構

補充:幾個概念的使用說明

  • 屬性 = 成員變數 = field = 域、欄位
  • 方法 = 成員方法 = 函式 = method
  • 建立類的物件 = 類的例項化 = 例項化類

4. 物件的建立與物件的記憶體解析

典型程式碼:

Person p1 = new Person();
Person p2 = new Person();
Person p3 = p1;//沒有新建立一個物件,共用一個堆空間中的物件實體。
/*說明:
*如果建立了一個類的多個物件,則每個物件都獨立的擁有一套類的屬性。(非static的)
*意味著:如果我們修改一個物件的屬性a,則不影響另外一個物件屬性a的值。
*/

記憶體解析:
【Java】類的結構

【Java】類的結構

5. JVM記憶體結構

編譯完源程式以後,生成一個或多個位元組碼檔案。我們使用JVM中的類的載入器和直譯器對生成的位元組碼檔案進行解釋執行。意味著,需要將位元組碼檔案對應的類載入到記憶體中,涉及到記憶體解析。
【Java】類的結構

虛擬機器棧:即為平時提到的棧結構。我們將區域性變數儲存在棧結構中

虛擬機器堆:我們將new出來的結構(比如:陣列、物件)載入在對空間中。

補充:物件的屬性(非static的)載入在堆空間中。

方法區:類的載入資訊、常量池、靜態域

6. 匿名物件:

我們建立的物件,沒顯式的賦給一個變數名。即為匿名物件

特點:匿名物件只能呼叫一次。

new Phone().sendEmail();
new Phone().playGame();
        
new Phone().price = 1999;
new Phone().showPrice();//0.0

應用場景:

PhoneMall mall = new PhoneMall();

//匿名物件的使用
mall.show(new Phone());
其中,
class PhoneMall{
    public void show(Phone phone){
        phone.sendEmail();
        phone.playGame();
    }
}

7. "萬事萬物皆物件"

在Java語言範疇中,我們都將功能、結構等封裝到類中,通過類的例項化,來呼叫具體的功能結構

  • Scanner,String等
  • 檔案:File
  • 網路資源:URL

涉及到Java語言與前端HTML、後端的資料庫互動時,前後端的結構在Java層面互動時,都體現為類、物件。

類的結構之一:屬性

1. 屬性 vs 區域性變數

1.1 相同點:

  • 定義變數的格式:資料型別 變數名 = 變數值

  • 先宣告,後使用

  • 變數都其對應的作用域

1.2 不同點:

1.2.1 在類中宣告的位置的不同

  • 屬性:直接定義在類的一對{}內
  • 區域性變數:宣告在方法內、方法形參、程式碼塊內、構造器形參、構造器內部的變數。

1.2.2 關於許可權修飾符的不同

  • 屬性:可以在宣告屬性時,指明其許可權,使用許可權修飾符。
  • 常用的許可權修飾符:private、public、預設、protected --->封裝性
  • 目前,宣告屬性時,使用預設就可以。
  • 區域性變數:不可以使用許可權修飾符。

1.2.3 預設初始化值的情況:

  • 屬性:類的屬性,根據其型別,都預設初始化值。
  • 整型(byte、short、int、long:0)
  • 浮點型(float、double:0.0)
  • 字元型(char:0 (或'\u0000'))
  • 布林型(boolean:false)
  • 引用資料型別(類、陣列、介面:null)
  • 區域性變數:沒預設初始化值。

    意味著,我們在呼叫區域性變數之前,一定要顯式賦值。
    特別地:形參在呼叫時,我們賦值即可。

1.2.4 在記憶體中載入的位置:

  • 屬性:載入到堆空間中 (非static)
  • 區域性變數:載入到棧空間

2. 變數的分類:

方式一:按照資料型別:
【Java】類的結構

方式二:按照在類中宣告的位置:
【Java】類的結構

類的結構之二:方法

定義:描述類應該具的功能。

1. 方法舉例:

1.1 JDK中的方法:

  • Math類:sqrt() random() ...
  • Scanner類:nextXxx() ...
  • Arrays類:sort() binarySearch() toString() equals() ...

1.2 自定義的方法:

public void eat(){
    System.out.printly("我要吃飯!!!");
}

public String getNation(String nation){
    System.out.printly("當前地點為" + nation);
}

1.3 方法的宣告:

許可權修飾符 返回值型別  方法名(形參列表){

      方法體

}

注意:static、final、abstract 來修飾的方法,後面再聊。

2. 方法的說明:

2.1 關於許可權修飾符:

Java規定的4種許可權修飾符:private、public、預設、protected

詳細內容請檢視文章《細說物件導向三大特徵》

2.2 返回值型別:

2.2.1 返回值 vs 沒返回值

  • 如果方法有返回值,則必須在方法宣告時,指定返回值的型別。同時,方法中,需要使用return關鍵字來返回指定型別的變數或常量:return 資料。
  • 如果方法沒返回值,則方法宣告時,使用void來表示。通常,沒返回值的方法中,就不需要使用return。如果使用的話,只能使用 return; 表示結束此方法的意思。

2.2.2 方法該不該定義返回值?

  1. 題目要求

  2. 具體問題具體分析

2.3 方法命名規範

  • 屬於識別符號,遵循識別符號的規則和規範,“見名知意”
  • 方法名應遵循小駝峰命名 aaaBbbCcc
  • 方法名最好使用英文單詞,不要使用拼音或者縮寫
  • 更多規範要求請參考《Java開發手冊》

2.4 關於形參列表

方法可以宣告0個,1個,或多個形參。

也可以使用可變形參,但可變形參必須放到最後,詳細說明請檢視本章第五部分。

格式:資料型別1 形參1, 資料型別2 形參2 .....

定義方法時,該不該定義形參?

  1. 題目要求

  2. 具體問題具體分析

2.5 方法體

是方法中功能的體現,通過迴圈分支、條件判斷等語句完成複雜的邏輯關係。

方法中可以呼叫其他方法,同類中可以直接呼叫,不同類中通過類的例項化物件呼叫。

注意:方法中不可以定義新的方法

3. 方法的使用

  • 同類中的方法可以直接呼叫當前類的屬性或方法,不同類中通過類的例項化物件呼叫。
  • 特殊的:方法A中又呼叫了自身---遞迴方法。(自身呼叫)

關於遞迴方法的使用請檢視本章第七部分。

4. 方法的過載

4.1 過載的概念

在同一個類中,允許存在一個以上的同名方法,只要它們的引數個數或者引數型別不同即可。

"兩同一不同":

  • 相同:同一個類、相同方法名
  • 引數列表不同:引數個數不同,引數型別不同

4.2 構成過載的例項:

// 舉例一:Arrays類中過載的sort() / binarySearch();PrintStream中的println()
// 舉例二:
// 如下的4個方法構成了過載
public void getSum(int i,int j){
    System.out.println("1");
}

public void getSum(double d1,double d2){
    System.out.println("2");
}

public void getSum(String s ,int i){
    System.out.println("3");
}

public void getSum(int i,String s){
    System.out.println("4");
}

不構成過載的例項:

// 如下的3個方法不能與上述4個方法構成過載
public int getSum(int i,int j){
    return 0;
}
    
public void getSum(int m,int n){
        
}
    
private void getSum(int i,int j){
    
}

4.3 過載方法判斷

如何判斷是否構成方法過載?
嚴格按照定義判斷:兩同一不同。跟方法的許可權修飾符、返回值型別、形參變數名、方法體都沒關係!

如何確定類中某一個方法的呼叫:
①方法名 ---> ②引數列表

5. 可變個數形參方法

5.1 使用說明

  • JDK 5.0新增的內容

  • JDK 5.0以前:採用陣列形參來定義方法,傳入多個同一型別變數
    public static void test(int a, String[] books);

  • JDK 5.0以後:採用可變個數形參來定義方法,傳入多個同一型別變數
    public static void test(int a, String ... books);

具體使用:

  • 可變個數形參的格式:資料型別... 變數名

  • 當呼叫可變個數形參的方法時,傳入的引數個數可以是:0個,1個,2個,.....

  • 可變個數形參的方法與本類中方法名相同,形參不同的方法之間構成過載

  • 可變個數形參的方法與本類中方法名相同,形參型別也相同的陣列之間不構成過載。換句話說,二者不能共存。

  • 可變個數形參在方法的形參中,必須宣告在末尾

  • 可變個數形參在方法的形參中,最多隻能宣告一個可變形參

5.2 舉例說明

public void show(int i){

}

public void show(String s){
    System.out.println("show(String)");
}

public void show(String ... strs){
    System.out.println("show(String ... strs)");

    for(int i = 0;i < strs.length;i++){
        System.out.println(strs[i]);
    }
}
// 不能與上一個方法同時存在
//  public void show(String[] strs){
//      
//  }
// 呼叫時:可變形參與陣列類似
test.show("hello");
test.show("hello","world");
test.show();

test.show(new String[]{"AA","BB","CC"});

6. Java的值傳遞機制

6.1 針對方法內變數的賦值舉例:

System.out.println("***********基本資料型別:****************");
int m = 10;
int n = m;

System.out.println("m = " + m + ", n = " + n);

n = 20;

System.out.println("m = " + m + ", n = " + n);

System.out.println("***********引用資料型別:****************");

Order o1 = new Order();
o1.orderId = 1001;

Order o2 = o1;//賦值以後,o1和o2的地址值相同,都指向了堆空間中同一個物件實體。

System.out.println("o1.orderId = " + o1.orderId + ",o2.orderId = " +o2.orderId);

o2.orderId = 1002;

System.out.println("o1.orderId = " + o1.orderId + ",o2.orderId = " +o2.orderId);

規則: 如果變數是基本資料型別,此時賦值的是變數所儲存的資料值。 如果變數是引用資料型別,此時賦值的是變數所儲存的資料的地址值。

6.2 針對於方法的引數概念

形參:方法定義時,宣告的小括號內的引數 實參:方法呼叫時,實際傳遞給形參的資料

6.3 Java中引數傳遞機制:值傳遞機制

規則:

  • 如果引數是基本資料型別,此時實參賦給形參的是實參真實儲存的資料值。
  • 如果引數是引用資料型別,此時實參賦給形參的是實參儲存資料的地址值。

推廣:

  • 如果變數是基本資料型別,此時賦值的是變數所儲存的資料值。
  • 如果變數是引用資料型別,此時賦值的是變數所儲存的資料的地址值。

6.4 記憶體解析:

記憶體解析畫法要點:

  1. 記憶體結構:棧(區域性變數)、堆(new出來的結構:物件(非static成員變數)、陣列
  2. 變數:成員變數 vs 區域性變數(方法內、方法形參、構造器內、構造器形參、程式碼塊內)

舉例一
【Java】類的結構

舉例二
【Java】類的結構

7. 遞迴方法

遞迴方法:一個方法體內呼叫它自身。

方法遞迴包含了一種隱式的迴圈,它會重複執行某段程式碼,但這種重複執行無須迴圈控制。 遞迴一定要向已知方向遞迴,否則這種遞迴就變成了無窮遞迴,類似於死迴圈。

遞迴方法舉例:

// 例1:計算1-n之間所自然數的和
public int getSum(int n) {// 3

    if (n == 1) {
        return 1;
    } else {
        return n + getSum(n - 1);
    }

}

// 例2:計算1-n之間所自然數的乘積:n!
public int getSum1(int n) {

    if (n == 1) {
        return 1;
    } else {
        return n * getSum1(n - 1);
    }

}

//例3:已知一個數列:f(0) = 1,f(1) = 4,f(n+2)=2*f(n+1) + f(n),
//其中n是大於0的整數,求f(10)的值。
public int f(int n){
    if(n == 0){
        return 1;
    }else if(n == 1){
        return 4;
    }else{
        //          return f(n + 2) - 2 * f(n + 1);
        return 2*f(n - 1) + f(n - 2);
    }
}

//例4:斐波那契數列

//例5:漢諾塔問題

//例6:快排

8. 方法的重寫

8.1 什麼是方法的重寫(override 或 overwrite)?

子類繼承父類以後,可以對父類中同名同引數的方法,進行覆蓋操作.

8.2 重寫的應用:

重寫以後,當建立子類物件以後,通過子類物件呼叫子父類中的同名同引數的方法時,實際執行的是子類重寫父類的方法。

8.3 重寫舉例:

// 父類
class Circle{
public double findArea(){}//求面積
}
// 子類
class Cylinder extends Circle{
public double findArea(){}//求表面積
}
**********************************************
// 父類
class Account{
public boolean withdraw(double amt){}
}
// 子類
class CheckAccount extends Account{
public boolean withdraw(double amt){}
}

8.4 重寫的規則:

方法的宣告:

許可權修飾符  返回值型別  方法名(形參列表) throws 異常的型別{
    //方法體
}

約定俗成:子類中的叫重寫的方法,父類中的叫被重寫的方法

  1. 子類重寫的方法的方法名和形參列表與父類被重寫的方法的方法名和形參列表相同
  2. 子類重寫的方法的許可權修飾符不小於父類被重寫的方法的許可權修飾符

    特殊情況:子類不能重寫父類中宣告為private許可權的方法

  3. 返回值型別:
    • 父類被重寫的方法的返回值型別是void,則子類重寫的方法的返回值型別只能是void
    • 父類被重寫的方法的返回值型別是A型別,則子類重寫的方法的返回值型別可以是A類或A類的子類
    • 父類被重寫的方法的返回值型別是基本資料型別(比如:double),則子類重寫的方法的返回值型別必須是相同的基本資料型別(必須也是double)
  4. 子類重寫的方法丟擲的異常型別不大於父類被重寫的方法丟擲的異常型別(具體放到異常處理時候講)

    子類和父類中的同名同引數的方法要麼都宣告為非static的(考慮重寫,要麼都宣告為static的(不是重寫)。

    開發中一般保持子父類一致

8.5 面試題:

區分方法的重寫和過載?

二者的概念:

  • 方法的重寫:子類繼承父類以後,可以對父類中同名同引數的方法,進行覆蓋操作.
  • 方法的過載:在同一個類中,允許存在一個以上的同名方法,只要它們的引數個數或者引數型別不同即可。

過載和重寫的具體規則:

  • 過載:兩同一不同,
  • 重寫:全相同,一模一樣

多型性:

  • 過載:不表現為多型性。
  • 重寫:表現為多型性。

從編譯和執行的角度看:

  • 過載,是指允許存在多個同名方法,而這些方法的引數不同。編譯器根據方法不同的參數列,對同名方法的名稱做修飾。對於編譯器而言,這些同名方法就成了不同的方法。它們的呼叫地址在編譯期就繫結了。Java的過載是可以包括父類和子類的,即子類可以過載父類的同名不同引數的方法。
  • 所以對於過載而言,在方法呼叫之前,編譯器就已經確定了所要呼叫的方法,這稱為“早繫結”或“靜態繫結”;
  • 而對於多型,只等到方法呼叫的那一刻,解釋執行器才會確定所要呼叫的具體方法,這稱為“晚繫結”或“動態繫結”。

    引用一句Bruce Eckel的話:“不要犯傻,如果它不是晚繫結,它就不是多型。”

類的結構之三:構造器

1. 構造器(或構造方法):Constructor

構造器的作用:(只要造物件就得用構造器)

  1. 建立物件

  2. 初始化物件的資訊

2. 使用說明:

  • 如果沒顯式的定義類的構造器的話,則系統預設提供一個空參的構造器
  • 定義構造器的格式:許可權修飾符 類名(形參列表){ }
  • 一個類中定義的多個構造器,彼此構成過載
  • 一旦我們顯式的定義了類的構造器之後,系統就不再提供預設的空參構造器
  • 一個類中,至少會有一個構造器

3. 構造器舉例:

//構造器不等於方法
public Person(){
    System.out.println("Person().....");
}

public Person(String n){
    name = n;

}

public Person(String n,int a){
    name = n;
    age = a;
}

構造器預設許可權和類的許可權一致

4. 屬性賦值順序

總結:屬性賦值的先後順序

  1. ① 預設初始化

  2. ② 顯式初始化

  3. ③ 構造器中初始化

  4. ④ 通過"物件.方法" 或 "物件.屬性"的方式,賦值

以上操作的先後順序:① - ② - ③ - ④

5. JavaBean的概念

所謂JavaBean,是指符合如下標準的Java類:

  • 類是公共的
  • 一個無參的公共的構造器
  • 屬性,且對應的get、set方法

類的結構之四:程式碼塊

程式碼塊(初始化塊)(重要性較屬性、方法、構造器差一些)

1. 程式碼塊的作用:

用來初始化類、物件的資訊

2. 分類:

程式碼塊要是使用修飾符,只能使用static 分類:靜態程式碼塊 vs 非靜態程式碼塊

3. 靜態程式碼塊 vs 非靜態程式碼塊區別

靜態程式碼塊:

  • 內部可以輸出語句
  • 隨著類的載入而執行,而且只執行一次
  • 作用:初始化類的資訊
  • 如果一個類中定義了多個靜態程式碼塊,則按照宣告的先後順序執行
  • 靜態程式碼塊的執行要優先於非靜態程式碼塊的執行
  • 靜態程式碼塊內只能呼叫靜態的屬性、靜態的方法,不能呼叫非靜態的結構

非靜態程式碼塊:

  • 內部可以輸出語句
  • 隨著物件的建立而執行
  • 每建立一個物件,就執行一次非靜態程式碼塊
  • 作用:可以在建立物件時,對物件的屬性等進行初始化
  • 如果一個類中定義了多個非靜態程式碼塊,則按照宣告的先後順序執行
  • 非靜態程式碼塊內可以呼叫靜態的屬性、靜態的方法,或非靜態的屬性、非靜態的方法

    注意:例項化子類物件時,涉及到父類、子類中靜態程式碼塊、非靜態程式碼塊、構造器的載入順序:由父及子,靜態先行。

舉例一

class Root{
    static{
        System.out.println("Root的靜態初始化塊");
    }
    {
        System.out.println("Root的普通初始化塊");
    }
    public Root(){
        System.out.println("Root的無引數的構造器");
    }
}
class Mid extends Root{
    static{
        System.out.println("Mid的靜態初始化塊");
    }
    {
        System.out.println("Mid的普通初始化塊");
    }
    public Mid(){
        System.out.println("Mid的無引數的構造器");
    }
    public Mid(String msg){
        //通過this呼叫同一類中過載的構造器
        this();
        System.out.println("Mid的帶引數構造器,其引數值:"
                           + msg);
    }
}
class Leaf extends Mid{
    static{
        System.out.println("Leaf的靜態初始化塊");
    }
    {
        System.out.println("Leaf的普通初始化塊");
    }   
    public Leaf(){
        //通過super呼叫父類中有一個字串引數的構造器
        super("呼叫父類構造器");
        System.out.println("Leaf的構造器");
    }
}
public class LeafTest{
    public static void main(String[] args){
        new Leaf(); 
        //new Leaf();
    }
}

舉例二

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

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

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

    public static void main(String[] args) { // 由父及子 靜態先行
        System.out.println("77777777777");
        System.out.println("************************");
        new Son();
        System.out.println("************************");

        new Son();
        System.out.println("************************");
        new Father();
    }
}

4. 屬性的賦值順序

  1. ① 預設初始化

  2. ② 顯式初始化 / ⑤在程式碼塊中賦值

  3. ③ 構造器中初始化

  4. ④ 有了物件以後,可以通過"物件.屬性"或"物件.方法"的方式,進行賦值

執行的先後順序:① - ② / ⑤ - ③ - ④

類的結構之五:內部類

內部類:類的第五個成員

1. 內部類的定義:

Java中允許將一個類A宣告在另一個類B中,則類A就是內部類,類B稱為外部類。

2. 內部類的分類:

成員內部類(靜態、非靜態 ) 區域性內部類(方法內、程式碼塊內、構造器內)

3. 成員內部類的理解:

一方面,作為外部類的成員:

  • 呼叫外部類的結構
  • 可以被static修飾
  • 可以被4種不同的許可權修飾

另一方面,作為一個類:

  • 類內可以定義屬性、方法、構造器等
  • 可以被final修飾,表示此類不能被繼承。言外之意,不使用final,就可以被繼承
  • 可以被abstract修飾

4. 成員內部類:

4.1 如何建立成員內部類的物件?(靜態的,非靜態的)

// 建立靜態的Dog內部類的例項(靜態的成員內部類):
Person.Dog dog = new Person.Dog();

// 建立非靜態的Bird內部類的例項(非靜態的成員內部類):
// Person.Bird bird = new Person.Bird();//錯誤的
Person p = new Person();
Person.Bird bird = p.new Bird();

4.2 如何在成員內部類中呼叫外部類的結構?

class Person{
    String name = "小明";
    public void eat(){
    }
    //非靜態成員內部類
    class Bird{
        String name = "杜鵑";
        public void display(String name){
            System.out.println(name);//方法的形參
            System.out.println(this.name);//內部類的屬性
            System.out.println(Person.this.name);//外部類的屬性
            //Person.this.eat();
        }
    }
}

5.區域性內部類的使用:

//返回一個實現了Comparable介面的類的物件
public Comparable getComparable(){

    //建立一個實現了Comparable介面的類:區域性內部類
    //方式一:
    //      class MyComparable implements Comparable{
    //
    //          @Override
    //          public int compareTo(Object o) {
    //              return 0;
    //          }
    //          
    //      }
    //      
    //      return new MyComparable();

    //方式二:
    return new Comparable(){

        @Override
        public int compareTo(Object o) {
            return 0;
        }
    };
}

注意點:

在區域性內部類的方法中(比如:show如果呼叫區域性內部類所宣告的方法(比如:method)中的區域性變數(比如:num)的話,要求此區域性變數宣告為final的。

原因:區域性內部類也會生成位元組碼檔案,在呼叫所屬類的區域性變數時,因為是兩個類,所以不能修改所屬類的屬性,因此所屬類將屬性設定為final的為內部類呼叫提供一個副本,而內部類不能進行修改。

  • jdk 7及之前版本:要求此區域性變數顯式的宣告為final的
  • jdk 8及之後的版本:可以省略final的宣告

總結:成員內部類和區域性內部類,在編譯以後,都會生成位元組碼檔案。
位元組碼檔名格式:
成員內部類:外部類$內部類名.class
區域性內部類:外部類$數字 內部類名.class

相關文章