[JAVA] Java物件導向之final、abstract抽象、和變數生命週期

老夫不正經發表於2020-04-02

Java物件導向之final、abstract抽象、和變數生命週期

Java物件導向之final、abstract抽象、和變數生命週期

final修飾符

final是最終、不可修改的意思, 在Java中它可以修飾非抽象類,非抽象方法和變數。但是需要注意的是:構造方法不能使用final修飾,因為構造方法不能夠被繼承。下面,我們們就來一一看看吧!

使用final關鍵字修飾類

先考慮下圖的程式碼例子:

final class

程式碼顯示錯誤,無法從SuperClass繼承,編譯器提示刪除final關鍵字;刪除final關鍵字後,程式碼正確無誤。

程式碼正確無誤

由此可得出:final修飾的類:,表示最終的類,,即該類不能再有子類,不能再被繼承。只要滿足以下條件就可以考慮把一個類設計成final類:

  1. 在設計之初就考慮不進入繼承體系的類。
  2. 出於安全考慮,類的實現細節不允許被擴充和修改。比如:基本資料型別的包裝類就是一個典型的例子。
  3. 該類不會再被擴充。

java裡final修飾的類有很多,比如八大基本資料型別的包裝類(Byte,Character、Short、Integer、Long、Float、Double、Boolean)和String等。

// Byte
public final class Byte extends Number implements Comparable<Byte> { }
// Character
public final class Character implements java.io.Serializable, Comparable<Character> { }
// Short
public final class Short extends Number implements Comparable<Short> { }
// Integer
public final class Integer extends Number implements Comparable<Integer> { }
// Long
public final class Long extends Number implements Comparable<Long> { }
// Float
public final class Float extends Number implements Comparable<Float> { }
// Double
public final class Double extends Number implements Comparable<Double> { }
// Boolean
public final class Boolean implements java.io.Serializable, Comparable<Boolean> { }
// String 
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { }
複製程式碼

使用final關鍵字修飾方法

如果用final關鍵字修飾方法呢?先考慮以下的程式碼:

final 方法

若是用final修飾方法,繼承該方法時會報編譯錯誤;刪除該關鍵字後,doWork()可被繼承,程式碼編譯通過;final修飾的方法為最終的方法,該方法不能被子類覆蓋,故也不能使用方法重寫。那麼什麼樣的情況下方法需要使用final修飾呢?

  1. 在父類中提供了統一的演算法骨架,不允許子類通過方法覆蓋來修改其實現細節, 此時使用final修飾。比如在模板方法設計模式中。
  2. 在構造器中呼叫的方法(初始化方法),此時一般使用final修飾。這也是構造器不能被繼承的原因。

注意: final修飾的方法了,子類可以呼叫,但是不能覆蓋(重寫)。

類常量:使用final關鍵字修飾的欄位

常量分類:

  1. 字面值常量(直接給出的資料值/直接量);比如:整數常量1,2,3,小數常量3.14,布林常量false,true等。
  2. final關鍵字修飾的常量。

final關鍵字修飾的常量

通過上述程式碼,不難看出,final關鍵字修飾的欄位無法被修改。通常開發中,我們建議final修飾的常量名用大寫字母表示,多個單詞之間使用下劃線(_)連線:如:

public static final String USER_NAME = "使用者名稱";
複製程式碼

且在Java中多個修飾符之間是沒有先後關係的,以下的三種修飾符排列順序都是ok的:

public static final 
或者 
public final static 
亦或者 
final static public 
複製程式碼

final修飾的變數是最終的變數,常量;該變數只能賦值一次,也只能在宣告時被初始化一次,不能被修改。在使用時需注意:

  1. final變數必須顯式地指定初始值,系統不會為final欄位初始化。
  2. final變數一旦賦予初始值,就不能再被重新賦值。
  3. 常量名規範:常量名符合識別符號,單詞全部使用大寫字母,如果是多個單片語成,多個單詞之間使用下劃線(_)連線。全域性靜態常量: public static final 修飾的變數,直接使用類名呼叫即可。

final修飾的引用型別變數到底表示引用的地址不能改變,還是其儲存的資料不能改變

  • 修飾基本型別變數:表示該變數的值不能改變,即不能用“=”號重新賦值。
  • 修飾引用型別變數:表示該變數的引用的地址不能變,而不是其儲存的資料內容不能變,其儲存的資料內容是可以被修改的。

什麼時候使用常量

  • 當在程式中,多個地方使用到共同的資料,而且該資料不會改變,此時可以將其定義全域性的常量;
  • 一般的,在開發中我們會專門定義一個常量類,專門用來儲存常量資料。

為何要使用final修飾符呢?在繼承關係中最大弊端就是會破壞封裝,子類能訪問父類的實現細節,,而且可以通過方法重寫(方法覆蓋)的方式修改方法的實現細節。且 final還是是唯一可以修飾區域性變數的修飾符。

抽象方法和抽象類

考慮如下的案例:求圓(Circle)、矩形(rectangle)的面積

求圓(Circle)、矩形(rectangle)的面積 初始程式碼設計

上述程式碼設計是存在問題的:

  1. 每一個圖形都有面積。但是不同圖形求面積的演算法是不一樣的,也就是說,每一個圖形的子類都必須去重寫getArea方法,如果不覆蓋,應該編譯報錯,無法計算其面積。
  2. 在圖形類(Graph)中定義了getArea方法,該方法不應該存在方法體,因為不同圖形子類求面積演算法不一樣,父類是不存在計算面積的方法的,故無法提供方法體。

案例:求圓(Circle)、矩形(rectangle)的面積 引入抽象的設計

案例:求圓(Circle)、矩形(rectangle)的面積 引入抽象的設計

抽象方法

使用abstract關鍵字修飾且沒有方法體的方法,稱為抽象方法。其特點是:

  1. 使用抽象abstract關鍵字修飾,方法沒有方法體,留給子類去實現/覆蓋其實現細節。
  2. 抽象方法修飾符不能是private 和 final以及static,因為抽象方法是要被重寫的;
  3. 抽象方法必須定義在抽象類或介面中。介面中的方法魔人都是使用public abstract 修飾的;

一般會把abstract寫在方法修飾符最前面,一看就知道是抽象方法;當然如果不這樣寫也沒錯。

抽象類

使用abstract關鍵字修飾的類,稱為抽象類。其特點是:

  1. 抽象類不能建立例項,也就是不能使用new建立一個抽象類物件,即使建立出抽象類物件,呼叫了抽象方法,也無法實現功能,因為抽象方法沒有方法體。
  2. 抽象類可以不包含抽象方法,倘若包含,哪怕是一個,該類也必須作為抽象類,抽象類可以包含普通方法,可以給子類呼叫;抽象類是有構造器的,且其子類構造器必須先呼叫父類構造器。
  3. 若子類沒有實現/覆蓋父類所有的抽象方法,那麼子類也得作為抽象類(抽象派生類)。
  4. 構造方法不能都定義成私有的,否則不能有子類,因為子類構造器無法呼叫其構造器(建立子類物件前先呼叫父類構造方法)。
  5. 抽象類不能使用final修飾,因為其必須有子類重寫其抽象方法,抽象方法才能得以實現。
  6. 抽象類是不完整的類,需作為父類,由子類實現其功能細節,功能才能得以實現。

抽象類在命名時,一般使用Abstract作為字首,讓呼叫者見名知義,看類名就知道其是抽象類。

抽象類中可以不存在抽象方法,這樣做雖然沒有太大的意義,但是可以防止外界建立其物件,所以我們會發現有些工具類沒有抽象方法,但卻是使用abstract來修飾類的。

普通類有的成員(方法、欄位、構造器),抽象類本質上也是一個類,故其都有。抽象類不能建立物件,但抽象類中是可以包含普通方法的。

變數生命週期

程式中的變數是用來儲存資料的,其又分為常量和變數兩種,關於變數的詳情可以檢視我的另一篇文章:[JAVA] Java 變數、表示式和資料型別詳解。定義變數的語法:

資料型別 變數名 = 值;
複製程式碼

變數根據在類中定義位置的不同,分成兩大類

成員變數: 全域性變數/欄位(Field),是定義在類中,方法作用域外的變數;可以先使用後定義(使用在前,定義在後)。

  1. 類成員變數:使用static修飾的欄位。
  2. 例項成員變數:也稱為物件變數,即沒有使用static修飾的欄位。

**區域性變數:**變數除了成員變數,其他都是區域性變數,主要體現在方法內,方法引數,程式碼塊內;區域性變數必須先定義而後才能使用。

  1. 方法內部的變數。
  2. 方法的形參。
  3. 程式碼塊中的變數,一對{}中的變數。

變數的初始值:變數只有在初始化後才會在記憶體中開闢空間。

成員變數: 預設是有初始值的。

成員變數的初始值

區域性變數: 沒有初始值。所以必須先初始化才能使用,而且其初始化是在方法執行開始時才進行的。

變數的作用域:變數根據定義的位置不同,也決定了各自的作用域是不同的,最直觀的就是看變數所在的那對花括號{},也就是離得最近的那對{}。成員變數的作用域在整個類中都有效。區域性變數的作用域在開始定義的位置開始,到緊跟著結束的花括號為止。

變數的生命週期

變數的作用域指的是變數的可使用的範圍,只有在這個範圍內,程式程式碼才能訪問它。當一個變數被定義時,它的作用域就確定了。變數的作用域決定了變數的生命週期,作用域不同,生命週期就不一樣。

變數的生命週期指的是一個變數被建立並分配記憶體空間開始,到該變數被銷燬並清除其所佔記憶體空間的過程。

package 關鍵字

在開發中,一個專案會有成百上千個Java檔案,如果所有的Java檔案都在一個目錄中,那麼管理起來就會很痛苦,很難想象這樣的專案會是什麼樣子。在Java中,引入了稱之為包(package)的概念。即:關鍵字:package ,專門用來給當前Java檔案設定包名(也就是名稱空間)。其語法格式如下:

package 包名.子包名.子包名; 
複製程式碼

必須把package語句作為Java檔案中的第一行程式碼,在所有程式碼之前。

package 語句和java編譯

在編譯java檔案時的編譯命令為:

javac -d . Hello.java
複製程式碼

如果此時Hello.java檔案中沒有使用package語句,表示在當前目錄中生成位元組碼檔案。執行時也不需要考慮包名。

如果此時Hello.java檔案中使用了package語句,此時表示在當前目錄中先生成包名目錄,再在包名目錄中生成位元組碼檔案。執行命令如下:

 java 包名.類名;
複製程式碼

package命名

  • a.自定義的包名不能以java開頭,會和java語言基礎類庫衝突。
  • b.包名必須遵循識別符號規範/全部小寫。
  • c.企業開發中,包名由公司域名倒寫來決定。
  • d.如果域名是以數字開頭的,不符合規範,可以考慮使用下劃線_開頭;但是在Android中,如果package中使用了_,則不能部署到模擬器上。此時,我們也可以使用一個字母來代替_。

package命名格式

package 域名倒寫.模組名.元件名;
複製程式碼

1.package下的類名:

  • 類的簡單名稱: PackageDemo.java
  • 類的全限定名稱: com._520.hello.PackageDemo.java

2.建議:先定義package名稱,再在定義的package內定義類。

import 關鍵字

當A類和B類不在同一個包中,若A類需要使用到B類中的功能,此時就得讓A類中去引入B類。使用import語句,把某個包下的類匯入到當前類中。

語法格式:    import 需要匯入類的全限定名;
複製程式碼

引入後在當前類中,只需要使用類的簡單名稱即可訪問。

如果我們需要引入包中的多個類,我們還得使用多個import語句,要寫很多次;此時可以使用萬用字元(*)

  • import 類的全限定名; 只能匯入某一個類。
  • import 包名.子包名.*; 表示會引入該包下的所有的在當前檔案中使用到的類。
  • import java.util.*; 表示匯入java.util包下的所有類。

注意:編譯器會預設匯入java.lang包下的類,但是並不會匯入java.lang的子包下的類。比如:java.lang.reflect.Method類,此時我們也得使用import java.lang.reflect.Method;來匯入Method類。

靜態import

**靜態import,**靜態匯入,是指將通過import static匯入其他類的靜態成員。以下程式碼例項:

package demo.importdir;
public class StaticDemo {	
    public static final int COUNT = 10;
}

package demo.dir;
import static demo.importdir.StaticDemo.COUNT;
public class StaticImportDemo {	
    public static void main(String[] args) {	
        System.out.println(COUNT);	
    }
}
複製程式碼

然後我們對StaticImportDemo反編譯,觀察JVM是如何處理靜態匯入的:

import java.io.PrintStream;
import demo.importdir.StaticDemo;
public class StaticImportDemo{	
    public StaticImportDemo() {	}	
    public static void main(String args[]) {	
        System.out.println(StaticDemo.COUNT);	
    }
}
複製程式碼

通過上述的反編譯程式碼,不難發現,其實所謂的靜態匯入也是一個語法糖/編譯器級別的新特性,其實在底層也是類名.靜態成員去訪問的。

所以在企業專案開始中不建議使用靜態匯入,容易引起欄位名,方法名混淆,不利於專案維護。

欄位不存在多型

通過物件呼叫欄位,在編譯時期就已經決定了呼叫哪一塊記憶體空間的資料。所以欄位不存在覆蓋的概念,也就是欄位不會有多型特徵,在執行時期體現的也會是子類特徵。

public class FieldDemo {		
    public static void main(String[] args) {	
        SubClass subClass = new SubClass();	
        System.out.println(subClass.name);	
    }
}

class SuperClass {	
    protected String name = "SuperClass.name";
}

class SubClass {	
    protected String name= "SubClass.name";
}

// 執行結果:SubClass.name
複製程式碼

通過執行上述程式碼,不難發現,當子類和父類存在相同的欄位的時候,無論修飾符是什麼(即使是private),都會在各自的記憶體空間中儲存資料,欄位並沒有體現出多型;

其實通過方法重寫字面意思也能發現其是針對方法的。所以只有方法才有覆蓋的概念,而欄位並不會被覆蓋。

程式碼塊

什麼是程式碼塊:在類或者在方法中,直接使用**"{}"**括起來的一段程式碼,表示一塊程式碼區域,我們將其稱為程式碼塊。程式碼塊裡變數屬於區域性變數,只在自己所在的作用域(所在的{})內有效。根據程式碼塊定義的位置的不同,我們又分成三種形式:

1.區域性程式碼塊:直接定義在方法內部的程式碼塊;一般不會直接使用區域性程式碼塊,而是會結合if,while,for,try等關鍵字配合使用,還有匿名內部類,表示一塊程式碼區域。示例如下:

if (true) {	......    }
複製程式碼

2.初始化程式碼塊(構造程式碼塊):定義在類中,每次建立物件的時候都會執行,並且是在構造器呼叫之前先執行本類中的初始化程式碼塊。但其實JVM在處理初始化程式碼塊時是將其移動到構造器中的最前面,從而達到先執行初始化程式碼塊,再執行構造器的功能。

在實際開發中,很少使用初始化程式碼塊;初始化操作會在構造器中進行,如果做初始化操作的程式碼比較複雜,可以另外定義一個方法做初始化操作,然後再在構造器中呼叫。

3.靜態程式碼塊:使用static修飾的初始化程式碼塊。格式如下:

class StaticDemo {		
    static {    	......    }    
}
複製程式碼

靜態程式碼塊會在主方法(main方法)執行之前執行,而且只執行一次。在Java中,main方法是程式的入口,靜態程式碼塊優先於main方法執行;是因為靜態成員是隨著位元組碼的載入而進入JVM中的,但此時此時main方法還沒執行,因為main方法需要JVM呼叫方能執行。

以下是一個程式碼塊的示例:

public class CodeBlockDemo {		
    {		
        System.out.println("執行初始化程式碼塊");	
    }		
    
    public CodeBlockDemo() {		
        System.out.println("執行無參構造器");	
    }		
    
    static {		
        System.out.println("執行靜態程式碼塊");	
    }		
    
    public static void main(String[] args) {		
        new CodeBlockDemo();		
        new CodeBlockDemo();		
        new CodeBlockDemo();	
    }
}
複製程式碼

其執行結果為:

執行靜態程式碼塊
執行初始化程式碼塊
執行無參構造器
執行初始化程式碼塊
執行無參構造器
執行初始化程式碼
塊執行無參構造器
複製程式碼

不難發現,呼叫順序依次為:靜態程式碼塊--》初始化程式碼塊--》構造器,且靜態程式碼塊只執行一次。然後再對上述示例程式碼做反編譯:

import java.io.PrintStream;
public class CodeBlockDemo{	
    public CodeBlockDemo()	{		
        System.out.println("執行初始化程式碼塊");		
        System.out.println("執行無參構造器");	
    }	
    
    public static void main(String args[])	{		
        new CodeBlockDemo();		
        new CodeBlockDemo();		
        new CodeBlockDemo();	
    }	
    
    static 	{		
        System.out.println("執行靜態程式碼塊");	
    }
    
}
複製程式碼

通過反編譯結果,發現JVM在處理初始化程式碼塊時是將初始化程式碼塊的程式碼移動到構造器中的最前面,從而達到先執行初始化程式碼塊,再執行構造器的功能。

完結。老夫雖不正經,但老夫一身的才華

相關文章