ch08_opp_intermediate

小兵学习笔记發表於2024-09-13
  • 第8章 物件導向程式設計(中級部分)
    • IDEA 常用快捷鍵
      • 包的三大作用
      • 包基本語法
      • 包的本質分析
      • 包的命名
      • 常用的包
      • 如何引入包
      • 注意事項和使用細節
    • 訪問修飾符
      • 基本介紹
      • 訪問修飾符的訪問範圍!
      • 使用的注意事項
    • 物件導向程式設計三大特徵
      • 基本介紹
      • 封裝介紹
      • 封裝的理解和好處
      • 封裝的實現步驟(三步)
      • 快速入門案例
      • 將構造器和setXxx 結合
    • 物件導向程式設計-繼承
      • 繼承的基本語法
      • 繼承的深入討論/細節問題
      • 繼承的本質分析!
    • super 關鍵字
      • 基本介紹
      • 基本語法
      • super 給程式設計帶來的便利/細節
      • super 和this 的比較
    • 方法重寫/覆蓋(override)
      • 注意事項和使用細節
      • 重寫和過載比較!
    • 物件導向程式設計-多型
      • 多[多種]態[狀態]基本介紹
      • 多型的具體體現
        • 方法的多型
        • 物件的多型!
      • 多型注意事項和細節討論
        • 多型的向上轉型
        • 多型向下轉型
        • 屬性沒有重寫之說
        • instanceOf 比較運算子
      • java 的動態繫結機制!!
      • 多型的應用
        • 多型陣列
        • 多型引數
    • Object 類詳解
      • equals 方法
        • ==和equals 的對比!!!!!
      • 如何重寫 equals 方法
      • hashCode 方法
        • 總結
      • toString 方法
      • finalize 方法
      • 斷點除錯(debug)
      • 斷點除錯介紹
      • 斷點除錯的快捷鍵
      • Idea debug進入 Jdk原始碼
      • 專案-零錢通

第8章 物件導向程式設計(中級部分)

IDEA 常用快捷鍵

  1. 刪除當前行, 預設是 ctrl + Y 自己配置 ctrl + d
  2. 複製當前行, 自己配置 ctrl + alt + 向下游標
  3. 補全程式碼 alt + /
  4. 新增註釋和取消註釋 ctrl + /
  5. 匯入該行需要的類先配置auto import , 然後使用 alt+enter 即可
  6. 快速格式化程式碼 ctrl + alt + L
  7. 快速執行程式自己定義 alt + R
  8. 生成構造器等 alt + insert [提高開發效率]
  9. 檢視一個類的層級關係 ctrl + H
  10. 將游標放在一個方法上,輸入 ctrl + B , 可以定位到方法
  11. 自動的分配變數名, 透過在後面加.var

包的三大作用

區分相同名字的類

當類很多時,可以很好的管理類[看Java API文件]

控制訪問範圍

包基本語法

package com.hspedu;

說明:

package關鍵字,表示打包

com.hspedu:表示包名

包的本質分析

包的本質實際上就是建立不同的資料夾/目錄來儲存類檔案

包的命名

命名規則

只能包含數字、字母、下劃線、小圓點.,但不能用數字開頭,不能是關鍵字或保留字。
命名規範

一般是小寫字母+小圓點

一般是 com.公司名.專案名.業務模組名

例如:

com.sina.crm.user //使用者模組

com.sina.crm.order //訂單模組

com.sina.crm.utils //工具類

常用的包

一個包下,包含很多的類,java 中常用的包有:

  1. java.lang.* //lang 包是基本包,預設引入,不需要再引入.
  2. java.util.* //util 包,系統提供的工具包, 工具類,使用Scanner
  3. java.net.* //網路包,網路開發
  4. java.awt.* //是做java 的介面開發,GUI

如何引入包

語法: import 包;

我們引入一個包的主要目的是要使用該包下的類

比如

import java.util.Scanner;  //就只是引入一個類

Scanner.import java.util.*;//表示將java.util包所有都引入

建議:我們需要使用到哪個類,就匯入哪個類即可,不建議使用*匯入

注意事項和使用細節

  1. package的作用是宣告當前類所在的包,需要放在類的最上面,一個類中最多隻有一句package
  2. import指令位置放在package的下面,在類定義前面,可以有多句且沒有順序要求。
//package的作用是宣告當前類所在的包,需要放在類(或者檔案)的最上面,
// 一個類中最多隻有一句package
package com.hspedu.pkg;

//import指令 位置放在package的下面,在類定義前面,可以有多句且沒有順序要求
import java.util.Scanner;
import java.util.Arrays;

訪問修飾符

基本介紹

java 提供四種訪問控制修飾符號,用於控制方法和屬性(成員變數)的訪問許可權(範圍):

  1. 公開級別: 用public 修飾,對外公開
  2. 受保護級別: 用protected 修飾, 對子類和同一個包中的類公開
  3. 預設級別: 沒有修飾符號,向同一個包的類公開.
  4. 私有級別: 用private修飾,只有類本身可以訪問,不對外公開.

訪問修飾符的訪問範圍!

使用的注意事項

  1. 修飾符可以用來修飾類中的屬性,成員方法以及類

  2. 只有預設的和public才能修飾類!,並且遵循上述訪問許可權的特點。

  3. 成員方法的訪問規則和屬性完全一樣.

物件導向程式設計三大特徵

基本介紹

物件導向程式設計有三大特徵:封裝、繼承和多型。

封裝介紹

封裝(encapsulation)就是把抽象出的資料 [屬性] 和對資料的操作 [方法] 封裝在一起,資料被保護在內部,程式的其它部分只有透過被授權的操作 [方法] ,才能對資料進行操作。

封裝的理解和好處

隱藏實現細節: 方法(連線資料庫) <-- 呼叫(傳入引數)

可以對資料進行驗證,保證安全合理

Person {name, age}
Person p = new Person();
p.name = "jack" ;
p.age= 1200;

封裝的實現步驟(三步)

  1. 將屬性進行私有化private【不能直接修改屬性】

  2. 提供一個公共的(public)set方法,用於對屬性判斷並賦值

    public void setXxx(型別引數名){//Xxx表示某個屬性
        //加入資料驗證的業務邏輯
        屬性=引數名;
    }
    
  3. 提供一個公共的 (public)get 方法,用於獲取屬性的值

    public 資料型別 getXxx(){ //許可權判斷,Xxx某個屬性
        return xx;
    }
    

快速入門案例

package com.hspedu.encap;

public class Encapsulation01 {

    public static void main(String[] args) {
        //如果要使用快捷鍵alt+r, 需要先配置主類
        //第一次,我們使用滑鼠點選形式運算程式,後面就可以用
        Person person = new Person();
        person.setName("韓順平");
        person.setAge(30);
        person.setSalary(30000);
        System.out.println(person.info());
        System.out.println(person.getSalary());

        //如果我們自己使用構造器指定屬性
        Person smith = new Person("smith", 80, 50000);
        System.out.println("====smith的資訊======");
        System.out.println(smith.info());


    }
}
/*
那麼在java中如何實現這種類似的控制呢?
請大家看一個小程式(com.hspedu.encap: Encapsulation01.java),
不能隨便檢視人的年齡,工資等隱私,並對設定的年齡進行合理的驗證。年齡合理就設定,否則給預設
年齡, 必須在 1-120, 年齡, 工資不能直接檢視 , name的長度在 2-6字元 之間

 */
class Person {
    public  String name; //名字公開
    private int age; //age 私有化
    private double salary; //..

    public void say(int n,String name) {

    }
    //構造器 alt+insert
    public Person() {
    }
    //有三個屬性的構造器
    public Person(String name, int age, double salary) {
//        this.name = name;
//        this.age = age;
//        this.salary = salary;
        //我們可以將set方法寫在構造器中,這樣仍然可以驗證
        setName(name);
        setAge(age);
        setSalary(salary);
    }

    // 自己寫setXxx 和 getXxx 太慢,我們使用快捷鍵 Generate --> Getter and Setter
    // 然後根據要求來完善我們的程式碼.
    public String getName() {
        return name;
    }
    public void setName(String name) {
        // 加入對資料的校驗,相當於增加了業務邏輯
        if(name.length() >= 2 && name.length() <=6 ) {
            this.name = name;
        }else {
            System.out.println("名字的長度不對,需要(2-6)個字元,預設名字");
            this.name = "無名人";
        }
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        //判斷
        if(age >= 1 && age <= 120) {//如果是合理範圍
            this.age = age;
        } else {
            System.out.println("你設定年齡不對,需要在 (1-120), 給預設年齡18 ");
            this.age = 18;//給一個預設年齡
        }
    }

    public double getSalary() {
        //可以這裡增加對當前物件的許可權判斷
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }
    //寫一個方法,返回屬性資訊
    public String info() {
        return "資訊為 name=" + name  + " age=" + age + " 薪水=" + salary;
    }
}

將構造器和setXxx 結合

可以將set方法寫在構造器中,這樣可以保證驗證。

public Person(String name, int age, double salary) {
    // this.name = name;
    // this.age = age;
    // this.salary = salary;
    //我們可以將set 方法寫在構造器中,這樣仍然可以驗證
    setName(name);
    setAge(age);
    setSalary(salary);
}

物件導向程式設計-繼承

繼承可以解決程式碼複用,讓我們的程式設計更加靠近人類思維.當多個類存在相同的屬性(變數)和方法時,可以從這些類中抽象出父類,在父類中定義這些相同的屬性和方法,所有的子類不需要重新定義這些屬性和方法,只需要透過extends 來宣告繼承父類即可。

繼承的基本語法

class 子類 extends 父類 {
}

1)子類就會自動擁有父類定義的屬性和方法

2)父類又叫超類,基類。

3)子類又叫派生類。

繼承的深入討論/細節問題

  1. 子類繼承了所有的屬性和方法,非私有的屬性和方法可以在子類直接訪問, 但是私有屬性和方法不能在子類直接訪問,要透過父類提供公共的方法去訪問
  2. 子類必須呼叫父類的構造器,完成父類的初始化。先呼叫父類構造器,再呼叫子類構造器。
  3. 當建立子類物件時,不管使用子類的哪個構造器,預設情況下總會去呼叫父類的無參構造器,如果父類沒有提供無參構造器,則必須在子類的構造器中用super 去指定使用父類的哪個構造器完成對父類的初始化工作,否則,編譯不會透過.
  4. 如果希望指定去呼叫父類的某個構造器,則顯式的呼叫一下: super(引數列表)
  5. super 在使用時,必須放在構造器第一行( super 只能在構造器中使用 )
  6. super() 和this() 都只能放在構造器第一行,因此這兩個方法不能共存在一個構造器
  7. java 所有類都是Object 類的子類, Object 是所有類的基類.
  8. 父類構造器的呼叫不限於直接父類!將一直往上追溯直到Object 類(頂級父類)
  9. 子類最多隻能繼承一個父類(指直接繼承),即java中是單繼承機制。
    思考:如何讓A 類繼承B類和C類? 方法:A 繼承B, B 繼承C。
  10. 不能濫用繼承,子類和父類之間必須滿足is-a 的邏輯關係
package com.hspedu.extend_;

import java.util.Arrays;

//輸入ctrl + H 可以看到類的繼承關係
public class Sub extends Base { //子類

    public Sub(String name, int age) {
        //1. 要呼叫父類的無參構造器, 如下或者什麼都不寫,預設就是呼叫super()
        //super();//父類的無參構造器
        //2. 要呼叫父類的 Base(String name) 構造器
        //super("hsp");
        //3. 要呼叫父類的 Base(String name, int age) 構造器
        super("king", 20);

        //細節:super在使用時,必須放在構造器第一行
        //細節: super() 和 this() 都只能放在構造器第一行,因此這兩個方法不能共存在一個構造器
        //this() 不能再使用了
        System.out.println("子類Sub(String name, int age)構造器被呼叫....");


    }

    public Sub() {//無參構造器
        //super(); //預設呼叫父類的無參構造器
        super("smith", 10);
        System.out.println("子類Sub()構造器被呼叫....");
    }
    //當建立子類物件時,不管使用子類的哪個構造器,預設情況下總會去呼叫父類的無參構造器
    public Sub(String name) {
        super("tom", 30);
        //do nothing...
        System.out.println("子類Sub(String name)構造器被呼叫....");
    }

    public void sayOk() {//子類方法
        //非私有的屬性和方法可以在子類直接訪問
        //但是私有屬性和方法不能在子類直接訪問
        System.out.println(n1 + " " + n2 + " " + n3);
        test100();
        test200();
        test300();
        //test400();錯誤
        //要透過父類提供公共的方法去訪問
        System.out.println("n4=" + getN4());
        callTest400();//
    }

}

繼承的本質分析!

我們著一個案例來分析當子類繼承父類,建立子類物件時,記憶體中到底發生了什麼?

當子類物件建立好後,建立查詢的關係

  1. 最先載入父類,分別是Object類,然後載入Grandpa,再Father,最後Son。

  2. 然後再分配堆空間:不同類的相同變數名不會衝突,堆中空間不同。

  3. 最後Son物件(0x11都是)返回給main中的引用。

那麼最後輸出什麼呢?(還是就近原則

package com.hspedu.extend_;

/**
 * 講解繼承的本質
 */
public class ExtendsTheory {
    public static void main(String[] args) {
        Son son = new Son();//記憶體的佈局
        //?-> 這時請大家注意,要按照查詢關係來返回資訊
        //(1) 首先看子類是否有該屬性
        //(2) 如果子類有這個屬性,並且可以訪問,則返回資訊
        //(3) 如果子類沒有這個屬性,就看父類有沒有這個屬性(如果父類有該屬性,並且可以訪問,就返回資訊..)
        //(4) 如果父類沒有就按照(3)的規則,繼續找上級父類,直到Object...
        System.out.println(son.name);//返回就是大頭兒子
        //System.out.println(son.age);//返回的就是39
        //System.out.println(son.getAge());//返回的就是39
        System.out.println(son.hobby);//返回的就是旅遊
    }
}

class GrandPa { //爺類
    String name = "大頭爺爺";
    String hobby = "旅遊";
}

class Father extends GrandPa {//父類
    String name = "大頭爸爸";
    private int age = 39;

    public int getAge() {
        return age;
    }
}

class Son extends Father { //子類
    String name = "大頭兒子";
}

super 關鍵字

基本介紹

super 代表父類的引用,用於訪問父類的屬性、方法、構造器

基本語法

1.訪問父類的屬性,但不能訪問父類的private屬性[案例]
super.屬性名;
2.訪問父類的方法,不能訪問父類的private方法
super.方法名(引數列表);
3.訪問父類的構造器:
super(引數列表);只能放在構造器的第一句,只能出現一句!

package com.hspedu.super_;

public class A extends Base{
    //4個屬性
    //public int n1 = 100;
    protected int n2 = 200;
    int 3 = 300;
    private int n4 = 400;

    public A() {}
    public A(String name) {}
    public A(String name, int age) {}

//    public void cal() {
//        System.out.println("A類的cal() 方法...");
//    }

    public void test100() {
    }

    protected void test200() {
    }

    void test300() {
    }

    private void test400() {
    }
}

cal() 和 this.cal() 相同,就近原則。
super.cal() 的順序是直接查詢父類,其他的規則一樣

package com.hspedu.super_;

public class B extends A {

    public int n1 = 888;

    //編寫測試方法
    public void test() {
        //super的訪問不限於直接父類,如果爺爺類和本類中有同名的成員,也可以使用super去訪問爺爺類的成員;
        // 如果多個基類(上級類)中都有同名的成員,使用super訪問遵循就近原則。A->B->C

        System.out.println("super.n1=" + super.n1);
        super.cal();
    }

    //訪問父類的屬性 , 但不能訪問父類的private屬性 [案例]super.屬性名
    public void hi() {
        System.out.println(super.n1 + " " + super.n2 + " " + super.n3 );
    }
    public void cal() {
        System.out.println("B類的cal() 方法...");
    }
    public void sum() {
        System.out.println("B類的sum()");
        //希望呼叫父類-A 的cal方法
        //這時,因為子類B沒有cal方法,因此我可以使用下面三種方式

        // !找cal方法時(cal() 和 this.cal()),順序是:
        // (1)先找本類,如果有,則呼叫
        // (2)如果沒有,則找父類(如果有,並可以呼叫,則呼叫)
        // (3)如果父類沒有,則繼續找父類的父類,整個規則,就是一樣的,直到 Object類
        // 提示:如果查詢方法的過程中,找到了,但是不能訪問, 則報錯, cannot access
        //      如果查詢方法的過程中,沒有找到,則提示方法不存在
        //cal();
        this.cal(); //等價 cal

        // !找cal方法(super.call()) 的順序是直接查詢父類,其他的規則一樣
        //super.cal();

        //演示訪問屬性的規則
        // !n1 和 this.n1 查詢的規則是
        //(1) 先找本類,如果有,則呼叫
        //(2) 如果沒有,則找父類(如果有,並可以呼叫,則呼叫)
        //(3) 如果父類沒有,則繼續找父類的父類,整個規則,就是一樣的,直到 Object類
        // 提示:如果查詢屬性的過程中,找到了,但是不能訪問, 則報錯, cannot access
        //      如果查詢屬性的過程中,沒有找到,則提示屬性不存在
        System.out.println(n1);
        System.out.println(this.n1);

        // !找n1 (super.n1) 的順序是直接查詢父類屬性,其他的規則一樣
        System.out.println(super.n1);

    }
    //訪問父類的方法,不能訪問父類的private方法 super.方法名(引數列表);
    public void ok() {
        super.test100();
        super.test200();
        super.test300();
        //super.test400();//不能訪問父類private方法
    }
    //訪問父類的構造器(這點前面用過):super(引數列表);只能放在構造器的第一句,只能出現一句!
    public  B() {
        //super();
        //super("jack", 10);
        super("jack");
    }
}

super 給程式設計帶來的便利/細節

  1. 呼叫父類的構造器的好處(分工明確,父類屬性由父類初始化,子類的屬性由子
    類初始化)

  2. 當子類中有和父類中的成員(屬性和方法)重名時,為了訪問父類的成員,必須
    透過super。如果沒有重名,使用super、this、直接訪問是一樣的效果!

  3. super的訪問不限於直接父類,如果爺爺類和本類中有同名的成員,也可以使用
    super去訪問爺爺類的成員; 如果多個基類(上級類)中都有同名的成員,使用super訪問遵循就近原則。A->B->C,當然也需要遵守訪問許可權的相關規則

super 和this 的比較

方法重寫/覆蓋(override)

簡單的說:方法覆蓋(重寫)就是子類有一個方法, 和父類的某個方法的名稱、返回型別、引數一樣,那麼我們就說子類的這個方法覆蓋了父類的方法。

注意事項和使用細節

方法重寫也叫方法覆蓋,需要滿足下面的條件

  1. 子類的方法的形參列表,方法名稱,要和父類方法的形參列表方法名稱完全一樣

  2. 子類方法的返回型別和父類方法返回型別一樣,或者是父類返回型別的子類

    比如交類返回型別是Object,子類方法返回型別是String
    public object getInfo()
    public String getInfo()

  3. 子類方法不能縮小父類方法的訪問許可權。最好大於等於父類的許可權。

    public > protected > 預設>private

重寫和過載比較!

物件導向程式設計-多型

傳統的方法帶來的問題是什麼?

程式碼的複用性不高,而且不利於程式碼維護

解決方案: 引出我們要講解的多型

多[多種]態[狀態]基本介紹

方法或物件具有多種形態。是物件導向的第三大特徵,多型是建立在封裝和繼承基礎之上的。

多型的具體體現

方法的多型

重寫和過載就體現多型

物件的多型!

(1) 一個物件的編譯型別和執行型別可以不一致

(2) 編譯型別在定義物件時,就確定了,不能改變

(3) 執行型別是可以變化的.

(4) 編譯型別看定義時 = 號的左邊,執行型別看 = 號的右邊

Animal animal = new Dog() // animal編譯型別是Animal,執行型別Dog
animal = new Cat();// animal的執行型別變成了Cat,編譯型別仍然是 Animal

多型注意事項和細節討論

多型的前提是:兩個物件(類)存在繼承關係

多型的向上轉型

  1. 本質:父類的引用指向了子類的物件

  2. 語法:父類型別引用名=new子類型別();

  3. 特點:編譯型別看左邊,執行型別看右邊。

    可以呼叫父類中的所有成員(需遵守訪問許可權),不能呼叫子類中特有成員;
    最終執行效果看子類的具體實現!
    (執行時看執行型別,例如找方法時就是採用就近原則)

    因為在編譯階段,能呼叫哪些成員,是由編譯型別決定的。

多型向下轉型

語法: 子類型別 引用名 = (子類型別) 父類引用;

  1. 只能強轉父類的引用,不能強轉父類的物件

  2. 要求父類的引用必須指向的是當前目標型別的物件

  3. 當向下轉型後,可以呼叫子類型別中所有的成員

package com.hspedu.poly_.detail_;

public class PolyDetail {
    public static void main(String[] args) {

        //向上轉型: 父類的引用指向了子類的物件
        //語法:父類型別引用名 = new 子類型別();
        Animal animal = new Cat();
        Object obj = new Cat();//可以嗎? 可以 Object 也是 Cat的父類

        //向上轉型呼叫方法的規則如下:
        //(1)可以呼叫父類中的所有成員(需遵守訪問許可權)
        //(2)但是不能呼叫子類的特有的成員
        //(#)因為在編譯階段,能呼叫哪些成員,是由編譯型別來決定的
        //animal.catchMouse();錯誤
        //(4)最終執行效果看子類(執行型別)的具體實現, 即呼叫方法時,按照從子類(執行型別)開始查詢方法
        //,然後呼叫,規則我前面我們講的方法呼叫規則一致。
        animal.eat();//貓吃魚..
        animal.run();//跑
        animal.show();//hello,你好
        animal.sleep();//睡

        //老師希望,可以呼叫Cat的 catchMouse方法
        //多型的向下轉型
        //(1)語法:子類型別 引用名 =(子類型別)父類引用;
        //問一個問題? cat 的編譯型別 Cat,執行型別是 Cat
        Cat cat = (Cat) animal;

        cat.catchMouse();//貓抓老鼠
        //(2)要求父類的引用必須指向的是當前目標型別的物件
        // animal 本來建立時就指向 cat物件
        // 後面 animal 向下轉型 cat 指向 cat物件
        Dog dog = (Dog) animal;//可以嗎? 錯誤!

        System.out.println("ok~~");
    }
}

屬性沒有重寫之說

屬性沒有重寫之說!屬性的值看編譯型別

package com.hspedu.poly_.detail_;

public class PolyDetail02 {
    public static void main(String[] args) {
        //屬性沒有重寫之說!屬性的值看編譯型別
        Base base = new Sub();//向上轉型
        System.out.println(base.count);// ? 看編譯型別 10
        Sub sub = new Sub();
        System.out.println(sub.count);//?  20
    }
}

class Base { //父類
    int count = 10;//屬性
}
class Sub extends Base {//子類
    int count = 20;//屬性
}

instanceOf 比較運算子

用於判斷物件的 執行型別 是否為 XX 型別 或 XX 型別的子型別

package com.hspedu.poly_.detail_;

public class PolyDetail03 {
    public static void main(String[] args) {
        BB bb = new BB();
        System.out.println(bb instanceof  BB);// true
        System.out.println(bb instanceof  AA);// true

        //aa 編譯型別 AA, 執行型別是BB
        //BB是AA子類
        AA aa = new BB();
        System.out.println(aa instanceof AA); // true
        System.out.println(aa instanceof BB); // true

        Object obj = new Object();
        System.out.println(obj instanceof AA);//false
        String str = "hello";
        //System.out.println(str instanceof AA);
        System.out.println(str instanceof Object);//true
    }
}

class AA {} //父類
class BB extends AA {}//子類

java 的動態繫結機制!!

1.當呼叫物件方法的時候,該方法會和該物件的記憶體地址/執行型別繫結
2.當呼叫物件屬性時,沒有動態繫結機制,哪裡宣告,哪裡使用
,找不到再去父類中尋找。

package com.hspedu.poly_.dynamic_;

public class DynamicBinding {
    public static void main(String[] args) {
        //a 的編譯型別 A, 執行型別 B
        A a = new B();//向上轉型
        System.out.println(a.sum()); //?40 -> 30 (20 + 10)
        System.out.println(a.sum1());//?30 -> 20 (10 + 10)
    }
}

class A {//父類
    public int i = 10;
    //動態繫結機制:

    public int sum() {//父類sum()
        return getI() + 10;//20 + 10
    }

    public int sum1() {//父類sum1()
        return i + 10;//10 + 10
    }

    public int getI() {//父類getI
        return i;
    }
}

class B extends A {//子類
    public int i = 20;

//    public int sum() {
//        return i + 20;
//    }

    public int getI() {//子類getI()
        return i;
    }

//    public int sum1() {
//        return i + 10;
//    }
}

多型的應用

多型陣列

陣列的定義型別為父類型別,裡面儲存的實際元素型別為子類型別。

應用例項:現有一個繼承結構如下:要求建立1 個Person 物件、2 個Student 物件和2 個Teacher 物件, 統一放在陣列中,並呼叫每個物件 say 方法.

應用例項升級:如何呼叫子類特有的方法,比如Teacher 有一個teach , Student 有一個study ,怎麼呼叫?

package com.hspedu.poly_.polyarr_;

public class PloyArray {
    public static void main(String[] args) {
        //應用例項:現有一個繼承結構如下:要求建立1個Person物件、
        // 2個Student 物件和2個Teacher物件, 統一放在陣列中,並呼叫每個物件say方法

        Person[] persons = new Person[5];
        persons[0] = new Person("jack", 20);
        persons[1] = new Student("mary", 18, 100);
        persons[2] = new Student("smith", 19, 30.1);
        persons[3] = new Teacher("scott", 30, 20000);
        persons[4] = new Teacher("king", 50, 25000);

        //迴圈遍歷多型陣列,呼叫say
        for (int i = 0; i < persons.length; i++) {
            //老師提示: person[i] 編譯型別是 Person ,執行型別是是根據實際情況有JVM來判斷
            System.out.println(persons[i].say());//動態繫結機制
            // 這裡聰明. 使用 型別判斷 + 向下轉型.!!!!
            if(persons[i]  instanceof  Student) {//判斷person[i] 的執行型別是不是Student
                Student student = (Student)persons[i];//向下轉型
                student.study();
                //小夥伴也可以使用一條語句 ((Student)persons[i]).study();
            } else if(persons[i] instanceof  Teacher) {
                Teacher teacher = (Teacher)persons[i];
                teacher.teach();
            } else if(persons[i] instanceof  Person){
                //System.out.println("你的型別有誤, 請自己檢查...");
            } else {
                System.out.println("你的型別有誤, 請自己檢查...");
            }
        }
    }
}

多型引數

方法定義的形參型別為父類型別,實參型別允許為子類型別應用例項

定義員工類Employee,包含姓名和月工資[private],以及計算年工資getAnnual的方法。普通員工和經理繼承了員工,經理類多了獎金bonus屬性和管理manage方法,普通員工類多了work方法,普通員工和經理類要求分別重寫getAnnual方法

測試類中新增一個方法showEmpAnnual(Employee e),實現獲取任何員工物件的年工資,並在main方法中呼叫該方法[e.getAnnual()]

測試類中新增一個方法,testWork,如果是普通員工,則呼叫work方法,如果是經理,則呼叫manage方法

package com.hspedu.poly_.polyarr_;

public class Person {//父類
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String say() {//返回名字和年齡
        return name + "\t" + age;
    }
}
package com.hspedu.poly_.polyarr_;

public class Student extends Person {
    private double score;

    public Student(String name, int age, double score) {
        super(name, age);
        this.score = score;
    }

    public double getScore() {
        return score;
    }

    public void setScore(double score) {
        this.score = score;
    }
    //重寫父類say

    @Override
    public String say() {
        return "學生 " + super.say() + " score=" + score;
    }
    //特有的方法
    public void study() {
        System.out.println("學生 " + getName() + " 正在學java...");
    }
}
package com.hspedu.poly_.polyarr_;

public class Teacher extends Person {
    private double salary;

    public Teacher(String name, int age, double salary) {
        super(name, age);
        this.salary = salary;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }
    //寫重寫父類的say方法

    @Override
    public String say() {
        return "老師 " + super.say() + " salary=" + salary;
    }
    //特有方法
    public void teach() {
        System.out.println("老師 " + getName() + " 正在講java課程...");
    }
}
package com.hspedu.poly_.polyarr_;

public class PloyArray {
    public static void main(String[] args) {
        //應用例項:現有一個繼承結構如下:要求建立1個Person物件、
        // 2個Student 物件和2個Teacher物件, 統一放在陣列中,並呼叫每個物件say方法

        Person[] persons = new Person[5];
        persons[0] = new Person("jack", 20);
        persons[1] = new Student("mary", 18, 100);
        persons[2] = new Student("smith", 19, 30.1);
        persons[3] = new Teacher("scott", 30, 20000);
        persons[4] = new Teacher("king", 50, 25000);

        //迴圈遍歷多型陣列,呼叫say
        for (int i = 0; i < persons.length; i++) {
            //老師提示: person[i] 編譯型別是 Person ,執行型別是是根據實際情況有JVM來判斷
            System.out.println(persons[i].say());//動態繫結機制
            //這裡大家聰明. 使用 型別判斷 + 向下轉型.
            if(persons[i]  instanceof  Student) {//判斷person[i] 的執行型別是不是Student
                Student student = (Student)persons[i];//向下轉型
                student.study();
                //小夥伴也可以使用一條語句 ((Student)persons[i]).study();
            } else if(persons[i] instanceof  Teacher) {
                Teacher teacher = (Teacher)persons[i];
                teacher.teach();
            } else if(persons[i] instanceof  Person){
                //System.out.println("你的型別有誤, 請自己檢查...");
            } else {
                System.out.println("你的型別有誤, 請自己檢查...");
            }
        }
    }
}

Object 類詳解

equals 方法

==和equals 的對比!!!!!

==是一個比較運算子

==:既可以判斷基本型別,又可以判斷引用型別

  • ==:如果判斷基本型別,判斷的是值是否相等。示例: int i=10; double d=10.0;

  • ==:如果判斷引用型別,判斷的是地址是否相等,即判定是不是同一個物件

equals:是Object類中的方法,只能判斷引用型別,看Jdk原始碼。(方法:游標放在方法上。然後按 ctrl + B 進行檢視原始碼)

  • 預設判斷的是地址是否相等,子類中往往重寫該方法,用於判斷內容是否相等。比如 Integer,String【看看String和 Integer的equals原始碼】
package com.hspedu.object_;

public class Equals01 {

    public static void main(String[] args) {
        A a = new A();
        A b = a;
        A c = b;
        System.out.println(a == c);//true
        System.out.println(b == c);//true
        // 編譯型別是B,但是本質上還是一個地址指向a
        B bObj = a;
        System.out.println(bObj == c);//true
        int num1 = 10;
        double num2 = 10.0;
        System.out.println(num1 == num2);// 基本資料型別,判斷值是否相等

        //equals 方法,原始碼怎麼檢視.
        //把游標放在equals方法,直接輸入ctrl+b
        //如果你使用不了. 自己配置. 即可使用.

        /*
        //帶大家看看Jdk的原始碼 String類的 equals方法
        //把Object的equals方法重寫了,變成了比較兩個字串值是否相同
        public boolean equals(Object anObject) {
        if (this == anObject) {//如果是同一個物件
            return true;//返回true
        }
        if (anObject instanceof String) {//判斷型別
            String anotherString = (String)anObject;//向下轉型
            int n = value.length;
            if (n == anotherString.value.length) {//如果長度相同
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {//然後一個一個的比較字元
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;//如果兩個字串的所有字元都相等,則返回true
            }
        }
        return false;//如果比較的不是字串,則直接返回false
    }
         */


        "hello".equals("abc");

        //看看Object類的 equals 是
        /*
        //即Object 的equals 方法預設就是比較物件地址是否相同
        //也就是判斷兩個物件是不是同一個物件.
         public boolean equals(Object obj) {
            return (this == obj);
        }
         */


        /*
        //從原始碼可以看到 Integer 也重寫了Object的equals方法,
        //變成了判斷兩個值是否相同
        public boolean equals(Object obj) {
            if (obj instanceof Integer) {
                return value == ((Integer)obj).intValue();
            }
            return false;
        }
         */
        Integer integer1 = new Integer(1000);
        Integer integer2 = new Integer(1000);
        System.out.println(integer1 == integer2);//false
        System.out.println(integer1.equals(integer2));//true

        String str1 = new String("hspedu");
        String str2 = new String("hspedu");
        System.out.println(str1 == str2);//false
        System.out.println(str1.equals(str2));//true
    }
}

class B {}
class A extends B {}

如何重寫 equals 方法

應用例項: 判斷兩個Person 物件的內容是否相等,如果兩個Person 物件的各個屬性值都一樣,則返回true,反之false。

檢視:com.hspedu.object_ EqualsExercise01

hashCode 方法

public int hashCode()

返回該物件的雜湊碼值。支援此方法是為了提高雜湊表(例如java.util.Hashtable 提供的雜湊表)的效能。

hashCode 的常規協定是:

  • 在 Java 應用程式執行期間,在對同一物件多次呼叫 hashCode 方法時,必須一致地返回相同的整數,前提是將物件進行 equals 比較時所用的資訊沒有被修改。從某一應用程式的一次執行到同一應用程式的另一次執行,該整數無需保持一致。
  • 如果根據 equals(Object) 方法,兩個物件是相等的,那麼對這兩個物件中的每個物件呼叫 hashCode 方法都必須生成相同的整數結果。
  • 如果根據 equals(java.lang.Object)
    方法,兩個物件不相等,那麼對這兩個物件中的任一物件上呼叫 hashCode 方法
    要求一定生成不同的整數結果。但是,程式設計師應該意識到,為不相等的物件生成不同整數結果可以提高雜湊表的效能。

實際上,由 Object 類定義的 hashCode
方法確實會針對不同的物件返回不同的整數。(這一般是透過將該物件的內部地址轉換成一個整數來實現的,但是 JavaTM 程式語言不需要這種實現技巧。)

返回:

此物件的一個雜湊碼值。

另請參見:

[equals(java.lang.Object)], [Hashtable]

總結

  1. 提高具有雜湊結構的容器的效率!
  2. 兩個引用,如果指向的是同一個物件,則雜湊值肯定是一樣的!
  3. 兩個引用,如果指向的是不同物件,則雜湊值是不一樣的(當然也可能存在碰撞)
  4. 雜湊值主要根據地址號來的! 不能完全將雜湊值等價於地址。(java跑在JVM上,無法真正拿到其內部地址)。
  5. 後面在集合,中hashCode 如果需要的話,也會重寫, 在講解集合時,具體看如何重寫hashCode()程式碼。

toString 方法

public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
  1. 基本介紹
    預設返回:全類名+@+雜湊值的十六進位制,子類往往重寫toString 方法,用於返回物件的屬性資訊(全類名就是包名 + 類名)

  2. 重寫toString 方法,列印物件或拼接物件時,都會自動呼叫該物件的toString 形式.

  3. 當直接輸出一個物件時, toString 方法會被預設的呼叫, 比如System.out.println(monster) ;// 就會預設呼叫monster.toString()

finalize 方法

  1. 當物件被回收時,系統自動呼叫該物件的finalize 方法。子類可以重寫該方法,做一些釋放資源(資料庫的連線,開啟或者關閉檔案)的操作。
  2. 什麼時候被回收:當某個物件沒有任何引用時,則jvm 就認為這個物件是一個垃圾物件,就會使用垃圾回收機制來銷燬該物件,在銷燬該物件前,會先呼叫finalize 方法。(當然並不是一有垃圾就立馬回收,有對應的垃圾回收GC演算法)。
  3. 垃圾回收機制的呼叫,是由系統來決定(即有自己的GC演算法), 也可以透過System.gc() 主動觸發垃圾回收機制。
  4. 我們在實際開發中,幾乎不會運用finalize , 所以更多就是為了應付面試。
package com.hspedu.object_;

//演示 Finalize的用法
public class Finalize_ {
    public static void main(String[] args) {

        Car bmw = new Car("寶馬");
        //這時 car物件就是一個垃圾,垃圾回收器就會回收(銷燬)物件, 在銷燬物件前,會呼叫該物件的finalize方法
        //,程式設計師就可以在 finalize中,寫自己的業務邏輯程式碼(比如釋放資源:資料庫連線,或者開啟檔案..)
        //,如果程式設計師不重寫 finalize,那麼就會呼叫 Object類的 finalize, 即預設處理
        //,如果程式設計師重寫了 finalize, 就可以實現自己的邏輯
        bmw = null;
        System.gc();//主動呼叫垃圾回收器

        System.out.println("程式退出了....");
    }
}
class Car {
    private String name;
    //屬性, 資源。。
    public Car(String name) {
        this.name = name;
    }
    //重寫finalize
    @Override
    protected void finalize() throws Throwable {
        System.out.println("我們銷燬 汽車" + name );
        System.out.println("釋放了某些資源...");
    }
}

斷點除錯(debug)

在斷點除錯過程中,是執行狀態,是以物件的執行型別來執行的.

A extends B; Bb = new A(); b.xx();

斷點除錯介紹

斷點除錯是指在程式的某一行設定一個斷點,除錯時,程式執行到這一行就會停住,然後你可以一步一步往下除錯,除錯過程中可以看各個變數當前的值,出錯的話,除錯到出錯的程式碼行即顯示錯誤,停下。進行分析從而找到這個Bug。

斷點除錯也能幫助我們檢視java底層原始碼的執行過程。

斷點除錯的快捷鍵

F7(跳入) F8(跳過) shift+F8(跳出) F9(resume,執行到下一個斷點)

F7:跳入方法內

F8: 逐行執行程式碼.

shift+F8: 跳出方法

Idea debug進入 Jdk原始碼

方法1:

使用force step into : 快捷鍵 alt + shift + F7

方法2:

這個配置一下就好了:點選Setting --> Build,Execution,Deployment --> Debugger --> Stepping 把Do not step into the classes中的java.,javax.取消勾選。

斷點可以在debug 過程中,動態的下斷點。(看複雜邏輯時常用)

專案-零錢通

package com.hspedu.smallchange;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;

public class SmallChangeSys {

    //化繁為簡
    //1. 先完成顯示選單,並可以選擇選單,給出對應提示
    //2. 完成零錢通明細
    //3. 完成收益入賬
    //4. 消費
    //5. 退出
    //6. 使用者輸入4退出時,給出提示"你確定要退出嗎? y/n",必須輸入正確的y/n ,否則迴圈輸入指令,直到輸入y 或者 n
    //7. 在收益入賬和消費時,判斷金額是否合理,並給出相應的提示
    public static void main(String[] args) {

        //定義相關的變數
        boolean loop = true;
        Scanner scanner = new Scanner(System.in);
        String key = "";

        //2. 完成零錢通明細
        //老韓思路, (1) 可以把收益入賬和消費,儲存到陣列 (2) 可以使用物件 (3) 簡單的話可以使用String拼接
        String details = "-----------------零錢通明細------------------";

        //3. 完成收益入賬  完成功能驅動程式設計師增加新的變化和程式碼
        //老韓思路, 定義新的變數
        double money = 0;
        double balance = 0;
        Date date = null; // date 是 java.util.Date 型別,表示日期
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); //可以用於日期格式化的

        //4. 消費
        //定義新變數,儲存消費的原因
        String note = "";
        do {

            System.out.println("\n================零錢通選單===============");
            System.out.println("\t\t\t1 零錢通明細");
            System.out.println("\t\t\t2 收益入賬");
            System.out.println("\t\t\t3 消費");
            System.out.println("\t\t\t4 退     出");

            System.out.print("請選擇(1-4): ");
            key = scanner.next();

            //使用switch 分支控制
            switch (key) {
                case "1":
                    System.out.println(details);
                    break;
                case "2":
                    System.out.print("收益入賬金額:");
                    money = scanner.nextDouble();
                    //money 的值範圍應該校驗 -》 一會在完善
                    //老師思路, 程式設計思想
                    //找出不正確的金額條件,然後給出提示, 就直接break
                    if(money <= 0) {
                        System.out.println("收益入賬金額 需要 大於 0");
                        break;
                    }
                    //找出正確金額的條件
                    balance += money;
                    //拼接收益入賬資訊到 details
                    date = new Date(); //獲取當前日期
                    details += "\n收益入賬\t+" + money + "\t" + sdf.format(date) + "\t" + balance;


                    break;
                case "3":
                    System.out.print("消費金額:");
                    money = scanner.nextDouble();
                    //money 的值範圍應該校驗 -》 一會在完善
                    //找出金額不正確的情況
                    //過關斬將 校驗方式.
                    if(money <= 0 || money > balance) {
                        System.out.println("你的消費金額 應該在 0-" + balance);
                        break;
                    }
                    System.out.print("消費說明:");
                    note = scanner.next();
                    balance -= money;
                    //拼接消費資訊到 details
                    date = new Date(); //獲取當前日期
                    details += "\n" + note + "\t-" + money + "\t" + sdf.format(date) + "\t" + balance;
                    break;
                case "4":
                    //使用者輸入4退出時,給出提示"你確定要退出嗎? y/n",必須輸入正確的y/n ,
                    // 否則迴圈輸入指令,直到輸入y 或者 n
                    // 老韓思路分析
                    // (1) 定義一個變數 choice, 接收使用者的輸入
                    // (2) 使用 while + break, 來處理接收到的輸入時 y 或者 n
                    // (3) 退出while後,再判斷choice是y還是n ,就可以決定是否退出
                    // (4) 建議一段程式碼,完成一個小功能,儘量不要混在一起
                    String choice = "";
                    while (true) { //要求使用者必須輸入y/n ,否則就一直迴圈
                        System.out.println("你確定要退出嗎? y/n");
                        choice = scanner.next();
                        if ("y".equals(choice) || "n".equals(choice)) {
                            break;
                        }
                        //第二個方案
//                        if("y".equals(choice)) {
//                            loop = false;
//                            break;
//                        } else if ("n".equals(choice)) {
//                            break;
//                        }
                    }

                    //當使用者退出while ,進行判斷
                    if (choice.equals("y")) {
                        loop = false;
                    }
                    break;
                default:
                    System.out.println("選擇有誤,請重新選擇");
            }

        } while (loop);

        System.out.println("-----退出了零錢通專案-----");

    }
}

改成OOP 版本,體會OOP 程式設計帶來的好處

package com.hspedu.smallchange.oop;

/**
 * 這裡我們直接呼叫SmallChangeSysOOP 物件,顯示主選單即可
 */
public class SmallChangeSysApp {

    public static void main(String[] args) {
        System.out.println("====hello公司====");
        new SmallChangeSysOOP().mainMenu();
    }
}

package com.hspedu.smallchange.oop;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;

/**
 * 該類是完成零錢通的各個功能的類
 * 使用OOP(物件導向程式設計)
 * 將各個功能對應一個方法.
 */
public class SmallChangeSysOOP {

    //屬性..
    //定義相關的變數
    boolean loop = true;
    Scanner scanner = new Scanner(System.in);
    String key = "";

    //2. 完成零錢通明細
    //老韓思路, (1) 可以把收益入賬和消費,儲存到陣列 (2) 可以使用物件 (3) 簡單的話可以使用String拼接
    String details = "-----------------零錢通明細------------------";

    //3. 完成收益入賬  完成功能驅動程式設計師增加新的變化和程式碼
    //老韓思路, 定義新的變數
    double money = 0;
    double balance = 0;
    Date date = null; // date 是 java.util.Date 型別,表示日期
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); //可以用於日期格式化的

    //4. 消費
    //定義新變數,儲存消費的原因
    String note = "";

    //先完成顯示選單,並可以選擇
    public void mainMenu() {
        do {

            System.out.println("\n================零錢通選單(OOP)===============");
            System.out.println("\t\t\t1 零錢通明細");
            System.out.println("\t\t\t2 收益入賬");
            System.out.println("\t\t\t3 消費");
            System.out.println("\t\t\t4 退     出");

            System.out.print("請選擇(1-4): ");
            key = scanner.next();

            //使用switch 分支控制
            switch (key) {
                case "1":
                    this.detail();
                    break;
                case "2":
                    this.income();
                    break;
                case "3":
                    this.pay();
                   break;
                case "4":
                    this.exit();
                    break;
                default:
                    System.out.println("選擇有誤,請重新選擇");
            }

        } while (loop);
    }

    //完成零錢通明細
    public void detail() {
        System.out.println(details);
    }
    //完成收益入賬
    public void income() {
        System.out.print("收益入賬金額:");
        money = scanner.nextDouble();
        //money 的值範圍應該校驗 -》 一會在完善
        //老師思路, 程式設計思想
        //找出不正確的金額條件,然後給出提示, 就直接return
        if(money <= 0) {
            System.out.println("收益入賬金額 需要 大於 0");
            return; //退出方法,不在執行後面的程式碼。
        }
        //找出正確金額的條件
        balance += money;
        //拼接收益入賬資訊到 details
        date = new Date(); //獲取當前日期
        details += "\n收益入賬\t+" + money + "\t" + sdf.format(date) + "\t" + balance;

    }
    //消費
    public void pay() {
        System.out.print("消費金額:");
        money = scanner.nextDouble();
        //money 的值範圍應該校驗 -》 一會在完善
        //找出金額不正確的情況
        //過關斬將 校驗方式.
        if(money <= 0 || money > balance) {
            System.out.println("你的消費金額 應該在 0-" + balance);
            return;
        }
        System.out.print("消費說明:");
        note = scanner.next();
        balance -= money;
        //拼接消費資訊到 details
        date = new Date(); //獲取當前日期
        details += "\n" + note + "\t-" + money + "\t" + sdf.format(date) + "\t" + balance;
    }

    //退出
    public void exit() {
        //使用者輸入4退出時,給出提示"你確定要退出嗎? y/n",必須輸入正確的y/n ,
        // 否則迴圈輸入指令,直到輸入y 或者 n
        // 老韓思路分析
        // (1) 定義一個變數 choice, 接收使用者的輸入
        // (2) 使用 while + break, 來處理接收到的輸入時 y 或者 n
        // (3) 退出while後,再判斷choice是y還是n ,就可以決定是否退出
        // (4) 建議一段程式碼,完成一個小功能,儘量不要混在一起
        String choice = "";
        while (true) { //要求使用者必須輸入y/n ,否則就一直迴圈
            System.out.println("你確定要退出嗎? y/n");
            choice = scanner.next();
            if ("y".equals(choice) || "n".equals(choice)) {
                break;
            }
            //第二個方案
//                        if("y".equals(choice)) {
//                            loop = false;
//                            break;
//                        } else if ("n".equals(choice)) {
//                            break;
//                        }
        }

        //當使用者退出while ,進行判斷
        if (choice.equals("y")) {
            loop = false;
        }
    }
}