1. 程式人生 > 其它 >Java物件和多型 (面向物件)

Java物件和多型 (面向物件)

目錄

Java物件和多型 (面向物件)

面向物件基礎

面向物件程式設計(Object Oriented Programming)

物件基於類建立,類相當於一個模板,物件就是根據模板創建出來的實體(就像做月餅,我們要做一個月餅首先需要一個模具,模具就是我們的類,而做出來的月餅,就是類的實現,也叫做物件),類是抽象的資料型別,並不能代表某一個具體的事物,類是物件的一個模板。類具有自己的屬性,包括成員變數、成員方法等,我們可以呼叫類的成員方法來讓類進行一些操作。

Scanner sc = new Scanner(System.in);
String str = sc.nextLine();
System.out.println("你輸入了:"+str);
sc.close();

所有的物件,都需要通過new關鍵字建立,基本資料型別不是物件!Java不是純面對物件語言!

不是基本型別的變數,都是引用型別,引用型別變數代表一個物件,而基本資料型別變數,儲存的是基本資料型別的值,我們可以通過引用來對物件進行操作。(最好不要理解為引用指向物件的地址,初學者不要談記憶體,學到JVM時再來討論)

物件佔用的記憶體由JVM統一管理,不需要手動釋放記憶體,當一個物件不再使用時(比如失去引用或是離開了作用域)會被JVM自動清理,記憶體管理更方便!


類的基本結構

為了快速掌握,我們自己建立一個自己的類,建立的類檔名稱應該和類名一致。

成員變數

在類中,可以包含許多的成員變數,也叫成員屬性,成員欄位(field)通過.

來訪問我們類中的成員變數,我們可以通過類建立的物件來訪問和修改這些變數。成員變數是屬於物件的!

public class Test {
    int age;
    String name;
}

public static void main(String[] args) {
    Test test = new Test();
    test.name = "奧利給";
    System.out.println(test.name);
}

成員變數預設帶有初始值,也可以自己定義初始值。

成員方法

我們之前的學習中接觸過方法(Method)嗎?主方法!

public static void main(String[] args) {
  //Body
}

方法是語句的集合,是為了完成某件事情而存在的。完成某件事情,可以有結果,也可以做了就做了,不返回結果。比如計算兩個數字的和,我們需要得到計算後的結果,所以說方法需要有返回值;又比如,我們只想吧數字列印在控制檯,只需要列印就行,不用給我結果,所以說方法不需要有返回值。

方法的定義和使用

在類中,我們可以定義自己的方法,格式如下:

[返回值型別] 方法名稱([引數]){
  //方法體
  return 結果;
}
  • 返回值型別:可以是引用型別和基本型別,還可以是void,表示沒有返回值
  • 方法名稱:和識別符號的規則一致,和變數一樣,規範小寫字母開頭!
  • 引數:例如方法需要計算兩個數的和,那麼我們就要把兩個數到底是什麼告訴方法,那麼它們就可以作為引數傳入方法
  • 方法體:方法具體要乾的事情
  • 結果:方法執行的結果通過return返回(如果返回型別為void,可以省略return)

非void方法中,return關鍵字不一定需要放在最後,但是一定要保證方法在任何情況下都具有返回值!

int test(int a){
  if(a > 0){
    //缺少retrun語句!
  }else{
    return 0;
  }
}

return也能用來提前結束整個方法,無論此時程式執行到何處,無論return位於哪裡,都會立即結束個方法!

void main(String[] args) {
   for (int i = 0; i < 10; i++) {
       if(i == 1) return;   //在迴圈內返回了!和break區別?
   }
   System.out.println("淦");   //還會到這裡嗎?
}

傳入方法的引數,如果是基本型別,會在呼叫方法的時候,對引數的值進行復制,方法中的引數變數,不是我們傳入的變數本身!

public static void main(String[] args) {
    int a = 10, b = 20;
  	new Test().swap(a, b);
  	System.out.println("a="+a+", b="+b);
}

public class Test{
 	void swap(int a, int b){  //傳遞的僅僅是值而已!
  		int temp = a;
  		a = b;
 			b = temp;
	} 
}

傳入方法的引數,如果是引用型別,那麼傳入的依然是該物件的引用!(類似於C語言的指標)

public class B{
 	String name;
}

public class A{
 	void test(B b){  //傳遞的是物件的引用,而不是值
    System.out.println(b.name);
  }
}

public static void main(String[] args) {
    int a = 10, b = 20;
  	B b = new B();
  	b.name = "lbw";
  	new A().test(b);
  	System.out.println("a="+a+", b="+b);
}

方法之間可以相互呼叫

void a(){
  //xxxx
}

void b(){
  a();
}

當方法在自己內部呼叫自己時,稱為遞迴呼叫(遞迴很危險,慎重!)

int a(){
  return a();
}

成員方法和成員變數一樣,是屬於物件的,只能通過物件去呼叫!


物件設計練習

  • 學生應該具有以下屬性:名字、年齡
  • 學生應該具有以下行為:學習、運動、說話

方法的過載

一個類中可以包含多個同名的方法,但是需要的形式引數不一樣。(補充:形式引數就是定義方法需要的引數,實際引數就傳入的引數)方法的返回型別,可以相同,也可以不同,但是僅返回型別不同,是不允許的!

public class Test {
    int a(){   //原本的方法
       return 1;
    }

    int a(int i){  //ok,形參不同
        return i;
    }
    
    void a(byte i){  //ok,返回型別和形參都不同
        
    }
    
    void a(){  //錯誤,僅返回值型別名稱不同不能過載
        
    }
}

現在我們就可以使用不同的引數,但是支援呼叫同樣的方法,執行一樣的邏輯:

public class Test {
    int sum(int a, int b){   //只有int支援,不靈活!
        return a+b;
    }
    
    double sum(double a, double b){  //重寫一個double型別的,就支援小數計算了
        return a+b;
    }
}

現在我們有很多種重寫的方法,那麼傳入實參後,到底進了哪個方法呢?

public class Test {
    void a(int i){
        System.out.println("呼叫了int");
    }

    void a(short i){
        System.out.println("呼叫了short");
    }

    void a(long i){
        System.out.println("呼叫了long");
    }

    void a(char i){
        System.out.println("呼叫了char");
    }

    void a(double i){
        System.out.println("呼叫了double");
    }

    void a(float i){
        System.out.println("呼叫了float");
    }
  
  	public static void main(String[] args) {
        Test test = new Test();
        test.a(1);   //直接輸入整數
        test.a(1.0);  //直接輸入小數

        short s = 2;
        test.a(s);  //會對號入座嗎?
        test.a(1.0F);
    }
}

構造方法

構造方法(構造器)沒有返回值,也可以理解為,返回的是當前物件的引用!每一個類都預設自帶一個無參構造方法。

//反編譯結果
package com.test;

public class Test {
    public Test() {    //即使你什麼都不編寫,也自帶一個無參構造方法,只是預設是隱藏的
    }
}

反編譯其實就是把我們編譯好的class檔案變回Java原始碼。

Test test = new Test();  //實際上存在Test()這個的方法,new關鍵字就是用來建立並得到引用的
// new + 你想要使用的構造方法

這種方法沒有寫明返回值,但是每個類都必須具有這個方法!只有呼叫類的構造方法,才能建立類的物件!

類要在一開始準備的所有東西,都會在構造方法裡面執行,完成構造方法的內容後,才能創建出物件!

一般最常用的就是給成員屬性賦初始值:

public class Student {
    String name;
    
    Student(){
        name = "傘兵一號";
    }
}

我們可以手動指定有參構造,當遇到名稱衝突時,需要用到this關鍵字

public class Student {
    String name;

    Student(String name){   //形參和類成員變數衝突了,Java會優先使用形式引數定義的變數!
        this.name = name;  //通過this指代當前的物件屬性,this就代表當前物件
    }
}

//idea 右鍵快速生成!

注意,this只能用於指代當前物件的內容,因此,只有屬於物件擁有的部分才可以使用this,也就是說,只能在類的成員方法中使用this,不能在靜態方法中使用this關鍵字。

在我們定義了新的有參構造之後,預設的無參構造會被覆蓋!

//反編譯後依然只有我們定義的有參構造!

如果同時需要有參和無參構造,那麼就需要用到方法的過載!手動再去定義一個無參構造。

public class Student {
    String name;

    Student(){

    }

    Student(String name){
        this.name = name;
    }
}

成員變數的初始化始終在構造方法執行之前

public class Student {
    String a = "sadasa";

    Student(){
        System.out.println(a);
    }

    public static void main(String[] args) {
        Student s = new Student();
    }
}

靜態變數和靜態方法

靜態變數和靜態方法是類具有的屬性(後面還會提到靜態類、靜態程式碼塊),也可以理解為是所有物件共享的內容。我們通過使用static關鍵字來宣告一個變數或一個方法為靜態的,一旦被宣告為靜態,那麼通過這個類建立的所有物件,操作的都是同一個目標,也就是說,物件再多,也只有這一個靜態的變數或方法。那麼,一個物件改變了靜態變數的值,那麼其他的物件讀取的就是被改變的值。

public class Student {
    static int a;
}

public static void main(String[] args) {
	Student s1 = new Student();
	s1.a = 10;
	Student s2 = new Student();
	System.out.println(s2.a);
}

不推薦使用物件來呼叫,被標記為靜態的內容,可以直接通過類名.xxx的形式訪問

public static void main(String[] args) {
   Student.a = 10;
   System.out.println(Student.a);
}

簡述類載入機制

類並不是在一開始就全部載入好,而是在需要時才會去載入(提升速度)以下情況會載入類:

  • 訪問類的靜態變數,或者為靜態變數賦值
  • new 建立類的例項(隱式載入)
  • 呼叫類的靜態方法
  • 子類初始化時
  • 其他的情況會在講到反射時介紹

所有被標記為靜態的內容,會在類剛載入的時候就分配,而不是在物件建立的時候分配,所以說靜態內容一定會在第一個物件初始化之前完成載入。

public class Student {
    static int a = test();  //直接呼叫靜態方法,只能呼叫靜態方法

    Student(){
        System.out.println("構造類物件");
    }

    static int test(){   //靜態方法剛載入時就有了
        System.out.println("初始化變數a");
        return 1;
    }
}

思考:下面這種情況下,程式能正常執行嗎?如果能,會輸出什麼內容?

public class Student {
    static int a = test();

    static int test(){
        return a;
    }

    public static void main(String[] args) {
        System.out.println(Student.a);
    }
}

定義和賦值是兩個階段,在定義時會使用預設值(上面講的,類的成員變數會有預設值)定義出來之後,如果發現有賦值語句,再進行賦值,而這時,呼叫了靜態方法,所以說會先去載入靜態方法,靜態方法呼叫時拿到a,而a這時僅僅是剛定義,所以說還是初始值,最後得到0

程式碼塊和靜態程式碼塊

程式碼塊在物件建立時執行,也是屬於類的內容,但是它在構造方法執行之前執行(和成員變數初始值一樣),且每建立一個物件時,只執行一次!(相當於構造之前的準備工作)

public class Student {
    {
        System.out.println("我是程式碼塊");
    }

    Student(){
        System.out.println("我是構造方法");
    }
}

靜態程式碼塊和上面的靜態方法和靜態變數一樣,在類剛載入時就會呼叫;

public class Student {
    static int a;

    static {
        a = 10;
    }
    
    public static void main(String[] args) {
        System.out.println(Student.a);
    }
}

String和StringBuilder類

字串類是一個比較特殊的類,他是Java中唯一過載運算子的類!(Java不支援運算子過載,String是特例)

String的物件直接支援使用++=運算子來進行拼接,並形成新的String物件!(String的字串是不可變的!)

String a = "dasdsa", b = "dasdasdsa";
String l = a+b;
System.out.println(l);

大量進行字串的拼接似乎不太好,編譯器是很聰明的,String的拼接有可能會被編譯器優化為StringBuilder來減少物件建立(物件頻繁建立時很費時間同時佔記憶體的!)

String result="String"+"and"; //會被優化成一句!
String str1="String";
String str2="and";
String result=str1+str2;
//變數隨時可變,在編譯時無法確定result的值,那麼只能在執行時再去確定
String str1="String";
String str2="and";
String result=(new StringBuilder(String.valueOf(str1))).append(str2).toString();
//使用StringBuilder,會採用類似於第一種實現,顯然會更快!

StringBuilder也是一個類,但是它能夠儲存可變長度的字串!

StringBuilder builder = new StringBuilder();
builder
       .append("a")
       .append("bc")
       .append("d");   //鏈式呼叫
String str = builder.toString();
System.out.println(str);

包和訪問控制

包宣告和匯入

包其實就是用來區分類位置的東西,也可以用來將我們的類進行分類,類似於C++中的namespace!

package com.test;

public class Test{
  
}

包其實是資料夾,比如com.test就是一個com資料夾中包含一個test資料夾,再包含我們Test類。

一般包按照個人或是公司域名的規則倒過來寫 頂級域名.一級域名.二級域名 com.java.xxxx

如果需要使用其他包裡面的類,那麼我們需要import(類似於C/C++中的include)

import com.test.Student;

也可以匯入包下的全部(一般匯入會由編譯器自帶幫我們補全,但是一定要記得我們需要導包!)

import com.test.*

Java預設為我們匯入了以下的包,不需要去宣告

import java.lang.*

靜態匯入

靜態匯入可以直接匯入某個類的靜態方法或者是靜態變數,匯入後,相當於這個方法或是類在定義在當前類中,可以直接呼叫該方法。

import static com.test.ui.Student.test;

public class Main {
    public static void main(String[] args) {
        test();
    }
}

靜態匯入不會進行類的初始化!

訪問控制

Java支援對類屬性訪問的保護,也就是說,不希望外部類訪問類中的屬性或是方法,只允許內部呼叫,這種情況下我們就需要用到許可權控制符。

![image-20210819160939950](/Users/nagocoler/Library/Application Support/typora-user-images/image-20210819160939950.png)

許可權控制符可以宣告在方法、成員變數、類前面,一旦宣告private,只能類內部訪問!

public class Student {
    private int a = 10;   //具有私有訪問許可權,只能類內部訪問
}

public static void main(String[] args) {
    Student s = new Student();
    System.out.println(s.a);  //還可以訪問嗎?
}

和檔名稱相同的類,只能是public,並且一個java檔案中只能有一個public class!

// Student.java
public class Student {
    
}
class Test{   //不能新增許可權修飾符!只能是default
	
}

陣列型別

假設出現一種情況,我想記錄100個數字,定義100個變數還可行嗎?

我們可以使用到陣列,陣列是相同型別資料的有序集合。陣列可以代表任何相同型別的一組內容(包括引用型別和基本型別)其中存放的每一個數據稱為陣列的一個元素,陣列的下標是從0開始,也就是第一個元素的索引是0!

int[] arr = new int[10];  //需要new關鍵字來建立!
String[] arr2 = new String[10];

陣列本身也是類(程式設計不可見,C++寫的),不是基本資料型別!

int[] arr = new int[10];
System.out.println(arr.length);   //陣列有成員變數!
System.out.println(arr.toString());   //陣列有成員方法!

一維陣列

一維陣列中,元素是依次排列的(線性),每個陣列元素可以通過下標來訪問!宣告格式如下:

型別[] 變數名稱 = new 型別[陣列大小];
型別 變數名稱n = new 型別[陣列大小];  //支援C語言樣式,但不推薦!

型別[] 變數名稱 = new 型別[]{...};  //靜態初始化(直接指定值和大小)
型別[] 變數名稱 = {...};   //同上,但是隻能在定義時賦值

創建出來的陣列每個元素都有預設值(規則和類的成員變數一樣,C語言建立的陣列需要手動設定預設值),我們可以通過下標去訪問:

int[] arr = new int[10];
arr[0] = 626;
System.out.println(arr[0]);
System.out.println(arr[1]);

我們可以通過陣列變數名稱.length來獲取當前陣列長度:

int[] arr = new int[]{1, 2, 3};
System.out.println(arr.length);  //列印length成員變數的值

陣列在建立時,就固定長度,不可更改!訪問超出陣列長度的內容,會出現錯誤!

String[] arr = new String[10];
System.out.println(arr[10]);  //出現異常!

//Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 11
//	at com.test.Application.main(Application.java:7)

思考:能不能直接修改length的值來實現動態擴容呢?

int[] arr = new int[]{1, 2, 3};
arr.length = 10;

陣列做實參,因為陣列也是類,所以形參得到的是陣列的引用而不是複製的陣列,操作的依然是陣列物件本身

public static void main(String[] args) {
    int[] arr = new int[]{1, 2, 3};
    test(arr);
    System.out.println(arr[0]);
}

private static void test(int[] arr){
    arr[0] = 2934;
}

陣列的遍歷

如果我們想要快速列印陣列中的每一個元素,又怎麼辦呢?

傳統for迴圈

我們很容易就聯想到for迴圈

int[] arr = new int[]{1, 2, 3};
for (int i = 0; i < arr.length; i++) {
   System.out.println(arr[i]);
}

foreach

傳統for迴圈雖然可控性高,但是不夠省事,要寫一大堆東西,有沒有一種省事的寫法呢?

int[] arr = new int[]{1, 2, 3};
for (int i : arr) {
    System.out.println(i);
}

foreach屬於增強型的for迴圈,它使得程式碼更簡潔,同時我們能直接拿到陣列中的每一個數字。

二維陣列

二維陣列其實就是存放陣列的陣列,每一個元素都存放一個數組的引用,也就相當於變成了一個平面。

//三行兩列
int[][] arr = { {1, 2},
                {3, 4},
                {5, 6}};
System.out.println(arr[2][1]);

二維陣列的遍歷同一維陣列一樣,只不過需要巢狀迴圈!

int[][] arr = new int[][]{ {1, 2},
                           {3, 4},
                           {5, 6}};
for (int i = 0; i < 3; i++) {
     for (int j = 0; j < 2; j++) {
          System.out.println(arr[i][j]);
     }
}

多維陣列

不止二維陣列,還存在三維陣列,也就是存放陣列的陣列的陣列,原理同二維陣列一樣,逐級訪問即可。

可變長引數

可變長引數其實就是陣列的一種應用,我們可以指定方法的形參為一個可變長引數,要求實參可以根據情況動態填入0個或多個,而不是固定的數量

public static void main(String[] args) {
     test("AAA", "BBB", "CCC");    //可變長,最後都會被自動封裝成一個數組
}
    
private static void test(String... test){
     System.out.println(test[0]);    //其實引數就是一個數組
}

由於是陣列,所以說只能使用一種型別的可變長引數,並且可變長引數只能放在最後一位!

實戰:三大基本排序演算法

現在我們有一個數組,但是數組裡面的資料是亂序排列的,如何使它變得有序?

int[] arr = {8, 5, 0, 1, 4, 9, 2, 3, 6, 7};

排序是程式設計的一個重要技能,掌握排序演算法,你的技術才能更上一層樓,很多的專案都需要用到排序!三大排序演算法:

  • 氣泡排序

氣泡排序就是冒泡,其實就是不斷使得我們無序陣列中的最大數向前移動,經歷n輪迴圈逐漸將每一個數推向最前。

  • 插入排序

插入排序其實就跟我們打牌是一樣的,我們在摸牌的時候,牌堆是亂序的,但是我們一張一張摸到手中進行排序,使得它變成了有序的!

  • 選擇排序

選擇排序其實就是每次都選擇當前陣列中最大的數排到最前面!


封裝、繼承和多型

封裝、繼承和多型是面向物件程式設計的三大特性。

封裝

封裝的目的是為了保證變數的安全性,使用者不必在意具體實現細節,而只是通過外部介面即可訪問類的成員,如果不進行封裝,類中的例項變數可以直接檢視和修改,可能給整個程式碼帶來不好的影響,因此在編寫類時一般將成員變數私有化,外部類需要同getter和setter方法來檢視和設定變數。

設想:學生小明已經建立成功,正常情況下能隨便改他的名字和年齡嗎?

public class Student {
    private String name;
    private int age;
  
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

也就是說,外部現在只能通過呼叫我定義的方法來獲取成員屬性,而我們可以在這個方法中進行一些額外的操作,比如小明可以修改名字,但是名字中不能包含"小"這個字。

public void setName(String name) {
    if(name.contains("小")) return;
    this.name = name;
}

單獨給外部開放設定名稱的方法,因為我還需要做一些額外的處理,所以說不能給外部直接操作成員變數的許可權!

封裝思想其實就是把實現細節給隱藏了,外部只需知道這個方法是什麼作用,而無需關心實現。

封裝就是通過訪問許可權控制來實現的。

繼承

繼承屬於非常重要的內容,在定義不同類的時候存在一些相同屬性,為了方便使用可以將這些共同屬性抽象成一個父類,在定義其他子類時可以繼承自該父類,減少程式碼的重複定義,子類可以使用父類中非私有的成員。

現在學生分為兩種,藝術生和體育生,他們都是學生的分支,但是他們都有自己的方法:

public class SportsStudent extends Student{   //通過extends關鍵字來繼承父類

    public SportsStudent(String name, int age) {
        super(name, age);   //必須先通過super關鍵字(指代父類),實現父類的構造方法!
    }

    public void exercise(){
        System.out.println("我超勇的!");
    }
}

public class ArtStudent extends Student{

    public ArtStudent(String name, int age) {
        super(name, age);
    }

    public void art(){
        System.out.println("隨手畫個畢加索!");
    }
}

子類具有父類的全部屬性,protected可見但外部無法使用(包括private屬性,不可見,無法使用),同時子類還能有自己的方法。繼承只能繼承一個父類,不支援多繼承!

每一個子類必須定義一個實現父類構造方法的構造方法,也就是需要在構造方法開始使用super(),如果父類使用的是預設構造方法,那麼子類不用手動指明。

所有類都預設繼承自Object類,除非手動指定型別,但是依然改變不了最頂層的父類是Object類。所有類都包含Object類中的方法,比如:

public static void main(String[] args) {
Object obj = new Object;
System.out.println(obj.hashCode());  //求物件的hashcode,預設是物件的記憶體地址
System.out.println(obj.equals(obj));  //比較物件是否相同,預設比較的是物件的記憶體地址,也就是等同於 ==
System.out.println(obj.toString());  //將物件轉換為字串,預設生成物件的類名稱+hashcode
}

關於Object類的其他方法,我們會在Java多執行緒中再來提及。

多型

多型是同一個行為具有多個不同表現形式或形態的能力。也就是同樣的方法,由於實現類不同,執行的結果也不同!

方法的重寫

我們之前學習了方法的過載,方法的重寫和過載是不一樣的,過載是原有的方法邏輯不變的情況下,支援更多引數的實現,而重寫是直接覆蓋原有方法!

//父類中的study
public void study(){
    System.out.println("學習");
}

//子類中的study
@Override  //宣告這個方法是重寫的,但是可以不要,我們現階段不接觸
public void study(){
    System.out.println("給你看點好康的");
}

再次定義同樣的方法後,父類的方法就被覆蓋!子類還可以給父類方法提升訪問許可權!

public static void main(String[] args) {
     SportsStudent student = new SportsStudent("lbw", 20);
     student.study();   //輸出子類定義的內容
}

思考:靜態方法能被重寫嗎?

當我們在重寫方法時,不僅想使用我們自己的邏輯,同時還希望執行父類的邏輯(也就是呼叫父類的方法)怎麼辦呢?

public void study(){
    super.study();
    System.out.println("給你看點好康的");
}

同理,如果想訪問父類的成員變數,也可以使用super關鍵字來訪問,注意,子類可以具有和父類相同的成員變數!而在方法中訪問的預設是 形參列表中 > 當前類的成員變數 > 父類成員變數

public void setTest(int test){
    test = 1;
  	this.test = 1;
  	super.test = 1;
}

再談型別轉換

我們曾經學習過基本資料型別的型別轉換,支援一種資料型別轉換為另一種資料型別,而我們的類也是支援型別轉換的(僅限於存在親緣關係的類之間進行轉換)比如子類可以直接向上轉型:

Student student = new SportsStudent("lbw", 20);  //父類變數引用子類例項
student.study();     //得到依然是具體實現的結果,而不是當前型別的結果

我們也可以把已經明確是由哪個類實現的父類引用,強制轉換為對應的型別:

Student student = new SportsStudent("lbw", 20);  //是由SportsStudent進行實現的
//... do something...

SportsStudent ps = (SportsStudent)student;  //讓它變成一個具體的子類
ps.sport();  //呼叫具體實現類的方法

這樣的型別轉換稱為向下轉型。

instanceof關鍵字

那麼我們如果只是得到一個父類引用,但是不知道它到底是哪一個子類的實現怎麼辦?我們可以使用instanceof關鍵字來實現,它能夠進行型別判斷!

private static void test(Student student){
    if (student instanceof SportsStudent){
        SportsStudent sportsStudent = (SportsStudent) student;
        sportsStudent.sport();
    }else if (student instanceof ArtStudent){
        ArtStudent artStudent = (ArtStudent) student;
        artStudent.art();
    }
}

通過進行型別判斷,我們就可以明確類的具體實現到底是哪個類!

思考:student instanceof Student的結果是什麼?

再談final關鍵字

我們目前只知道final關鍵字能夠使得一個變數的值不可更改,那麼如果在類前面宣告final,會發生什麼?

public final class Student {   //類被宣告為終態,那麼它還能被繼承嗎
  	
}

類一旦被宣告為終態,將無法再被繼承,不允許子類的存在!而方法被宣告為final呢?

public final void study(){  //還能重寫嗎
    System.out.println("學習");
}

如果類的成員屬性被宣告為final,那麼必須在構造方法中或是在定義時賦初始值!

private final String name;   //引用型別不允許再指向其他物件
private final int age;    //基本型別值不允許發生改變

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

學習完封裝繼承和多型之後,我們推薦在不會再發生改變的成員屬性上新增final關鍵字,JVM會對添加了final關鍵字的屬性進行優化!

抽象類

類本身就是一種抽象,而抽象類,把類還要抽象,也就是說,抽象類可以只保留特徵,而不保留具體呈現形態,比如方法可以定義好,但是我可以不去實現它,而是交由子類來進行實現!

public abstract class Student {    //抽象類
		public abstract void test();  //抽象方法
}

通過使用abstract關鍵字來表明一個類是一個抽象類,抽象類可以使用abstract關鍵字來表明一個方法為抽象方法,也可以定義普通方法,抽象方法不需要編寫具體實現(無方法體)但是必須由子類實現(除非子類也是一個抽象類)!

抽象類由於不是具體的類定義,因此無法直接通過new關鍵字來建立物件!

Student s = new Student(){    //只能直接建立帶實現的匿名內部類!
  public void test(){
    
  }
}

因此,抽象類一般只用作繼承使用!抽象類使得繼承關係之間更加明確:

public void study(){   //現在只能由子類編寫,父類沒有定義,更加明確了多型的定義!同一個方法多種實現!
    System.out.println("給你看點好康的");
}

介面

介面甚至比抽象類還抽象,他只代表某個確切的功能!也就是隻包含方法的定義,甚至都不是一個類!介面包含了一些列方法的具體定義,類可以實現這個介面,表示類支援介面代表的功能(類似於一個外掛,只能作為一個附屬功能加在主體上,同時具體實現還需要由主體來實現)

public interface Eat {
	void eat(); 
}

通過使用interface關鍵字來表明是一個介面(注意,這裡class關鍵字被替換為了interface)介面只能包含public許可權的抽象方法!(Java8以後可以有預設實現)我們可以通過宣告default關鍵字來給抽象方法一個預設實現:

public interface Eat {
    default void eat(){
        //do something...
    }
}

介面中定義的變數,預設為public static final

public interface Eat {
    int a = 1;
    void eat();
}

一個類可以實現很多個介面,但是不能理解為多繼承!(實際上實現介面是附加功能,和繼承的概念有一定出入,頂多說是多繼承的一種替代方案)一個類可以附加很多個功能!

public class SportsStudent extends Student implements Eat, ...{
		@Override
    public void eat() {
        
    }
}

類通過implements關鍵字來宣告實現的介面!每個介面之間用逗號隔開!

實現介面的類也能通過instanceof關鍵字判斷,也支援向上和向下轉型!

內部類

類中可以存在一個類!各種各樣的長相怪異的程式碼就是從這裡開始出現的!

成員內部類

我們的類中可以在巢狀一個類:

public class Test {
    class Inner{   //類中定義的一個內部類
        
    }
}

成員內部類和成員變數和成員方法一樣,都是屬於物件的,也就是說,必須存在外部物件,才能建立內部類的物件!

public static void main(String[] args) {
    Test test = new Test();
    Test.Inner inner = test.new Inner();   //寫法有那麼一絲怪異,但是沒毛病!
}

靜態內部類

靜態內部類其實就和類中的靜態變數和靜態方法一樣,是屬於類擁有的,我們可以直接通過類名.去訪問:

public class Test {
    static class Inner{

    }
}

public static void main(String[] args) {
    Test.Inner inner = new Test.Inner();   //不用再建立外部類物件了!
}

區域性內部類

對,你沒猜錯,就是和區域性變數一樣噠~

public class Test {
    public void test(){
        class Inner{

        }
        
        Inner inner = new Inner();
    }
}

反正我是沒用過!內部類 -> 累不累 -> 反正我累了!

匿名內部類

匿名內部類才是我們的重點,也是實現lambda表示式的原理!匿名內部類其實就是在new的時候,直接對介面或是抽象類的實現:

public static void main(String[] args) {
        Eat eat = new Eat() {
            @Override
            public void eat() {
                //DO something...
            }
        };
    }

我們不用單獨去建立一個類來實現,而是可以直接在new的時候寫對應的實現!但是,這樣寫,無法實現複用,只能在這裡使用!

lambda表示式

讀作λ表示式,它其實就是我們介面匿名實現的簡化,比如說:

public static void main(String[] args) {
        Eat eat = new Eat() {
            @Override
            public void eat() {
                //DO something...
            }
        };
    }

public static void main(String[] args) {
        Eat eat = () -> {};   //等價於上述內容
    }

lambda表示式(匿名內部類)只能訪問外部的final型別或是隱式final型別的區域性變數!

為了方便,JDK預設就為我們提供了專門寫函式式的介面,這裡只介紹Consumer

列舉類

假設現在我們想給小明新增一個狀態(跑步、學習、睡覺),外部可以實時獲取小明的狀態:

public class Student {
    private final String name;
    private final int age;
    private String status;
  
  	//...
  
  	public void setStatus(String status) {
        this.status = status;
    }

    public String getStatus() {
        return status;
    }
}

但是這樣會出現一個問題,如果我們僅僅是儲存字串,似乎外部可以不按照我們規則,傳入一些其他的字串。這顯然是不夠嚴謹的!

有沒有一種辦法,能夠更好地去實現這樣的狀態標記呢?我們希望開發者拿到使用的就是我們定義好的狀態,我們可以使用列舉類!

public enum Status {
    RUNNING, STUDY, SLEEP    //直接寫每個狀態的名字即可,分號可以不打,但是推薦打上
}

使用列舉類也非常方便,我們只需要直接訪問即可

public class Student {
    private final String name;
    private final int age;
    private Status status;
  
 		//...
  
  	public void setStatus(Status status) {   //不再是String,而是我們指定的列舉型別
        this.status = status;
    }

    public Status getStatus() {
        return status;
    }
}

public static void main(String[] args) {
    Student student = new Student("小明", 18);
    student.setStatus(Status.RUNNING);
    System.out.println(student.getStatus());
}

列舉型別使用起來就非常方便了,其實列舉型別的本質就是一個普通的類,但是它繼承自Enum類,我們定義的每一個狀態其實就是一個public static final的Status型別成員變數!

// Compiled from "Status.java"
public final class com.test.Status extends java.lang.Enum<com.test.Status> {
  public static final com.test.Status RUNNING;
  public static final com.test.Status STUDY;
  public static final com.test.Status SLEEP;
  public static com.test.Status[] values();
  public static com.test.Status valueOf(java.lang.String);
  static {};
}

既然列舉型別是普通的類,那麼我們也可以給列舉型別新增獨有的成員方法

public enum Status {
    RUNNING("睡覺"), STUDY("學習"), SLEEP("睡覺");   //無參構造方法被覆蓋,建立列舉需要新增引數(本質就是呼叫的構造方法!)

    private final String name;    //列舉的成員變數
    Status(String name){    //覆蓋原有構造方法(預設private,只能內部使用!)
        this.name = name;
    }
  
  	public String getName() {   //獲取封裝的成員變數
        return name;
    }
}

public static void main(String[] args) {
    Student student = new Student("小明", 18);
    student.setStatus(Status.RUNNING);
    System.out.println(student.getStatus().getName());
}

列舉類還自帶一些繼承下來的實用方法

Status.valueOf("")   //將名稱相同的字串轉換為列舉
Status.values()   //快速獲取所有的列舉

基本型別包裝類

Java並不是純面向物件的語言,雖然Java語言是一個面向物件的語言,但是Java中的基本資料型別卻不是面向物件的。在學習泛型和集合之前,基本型別的包裝類是一定要講解的內容!

我們的基本型別,如果想通過物件的形式去使用他們,Java提供的基本型別包裝類,使得Java能夠更好的體現面向物件的思想,同時也使得基本型別能夠支援物件操作!

  • byte -> Byte
  • boolean -> Boolean
  • short -> Short
  • char -> Character
  • int -> Integer
  • long -> Long
  • float -> Float
  • double -> Double

包裝類實際上就行將我們的基本資料型別,封裝成一個類(運用了封裝的思想)

private final int value;   //Integer內部其實本質還是存了一個基本型別的資料,但是我們不能直接操作

public Integer(int value) {
    this.value = value;
}

現在我們操作的就是Integer物件而不是一個int基本型別了!

public static void main(String[] args) {
     Integer i = 1;   //包裝型別可以直接接收對應型別的資料,並變為一個物件!
     System.out.println(i + i);    //包裝型別可以直接被當做一個基本型別進行操作!
}

自動裝箱和拆箱

那麼為什麼包裝型別能直接使用一個具體值來賦值呢?其實依靠的是自動裝箱和拆箱機制

Integer i = 1;    //其實這裡只是簡寫了而已
Integer i = Integer.valueOf(1);  //編譯後真正的樣子

呼叫valueOf來生成一個Integer物件!

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)   //注意,Java為了優化,有一個快取機制,如果是在-128~127之間的數,會直接使用已經快取好的物件,而不是再去建立新的!(面試常考)
       return IntegerCache.cache[i + (-IntegerCache.low)];
  	return new Integer(i);   //返回一個新建立好的物件
}

而如果使用包裝類來進行運算,或是賦值給一個基本型別變數,會進行自動拆箱:

public static void main(String[] args) {
    Integer i = Integer.valueOf(1);
    int a = i;    //簡寫
    int a = i.intValue();   //編譯後實際的程式碼
  
  	long c = i.longValue();   //其他型別也有!
}

既然現在是包裝型別了,那麼我們還能使用==來判斷兩個數是否相等嗎?

public static void main(String[] args) {
    Integer i1 = 28914;
    Integer i2 = 28914;

    System.out.println(i1 == i2);   //實際上判斷是兩個物件是否為同一個物件(記憶體地址是否相同)
    System.out.println(i1.equals(i2));   //這個才是真正的值判斷!
}

注意IntegerCache帶來的影響!

思考:下面這種情況結果會是什麼?

public static void main(String[] args) {
    Integer i1 = 28914;
    Integer i2 = 28914;

    System.out.println(i1+1 == i2+1);
}

在集合類的學習中,我們還會繼續用到我們的包裝型別!