天天看點

深入類和對象——深入Java基礎系列(二)前言1. 概述2. 類的生命周期3. 對象的生命周期

前言

本文從類和對象的基本概念出發,一方面,深入探讨了内部類,通過對比的方式介紹了不同類型的内部類的定義、特點、使用場景以及使用時的注意事項,更進一步了解了内部類相關特性的内在原理。另一方面,詳細的介紹了類的整個生命周期,徹底弄清楚類從加載到消亡的全過程。

目錄

前言

1. 概述

1.1 類和對象的概念

1.2 抽象類和内部類

1.3 深入了解内部類

2. 類的生命周期

2.1 加載

2.2 連接配接

2.3 初始化

2.4 使用

2.5 類解除安裝

3. 對象的生命周期

1. 概述

1.1 類和對象的概念

(1)類:類是對某種類型的事物的公共屬性和行為的抽取,他并不是實際存在的實體。

(2)對象:對象是類的一個執行個體,代表現實世界中可以明确辨別的一個實體。

可以這麼了解:在現實世界中我們會用“高富帥”、“白富美”描述一種群體,這隻是一種描述而沒有具體的指向。而志玲姐姐是個白富美,她就是個實體例子,也就是對象。

(3)匿名對象

  • 匿名對象:沒有引用類型變量指向的對象稱作為匿名對象。  
  • 執行個體:使用 java類描述一個學生類。  new Student(),左邊并沒有引用類型變量指向這個對象;
  • 匿名對象要注意的事項:  
a. 我們一般不會給匿名對象賦予屬性值,因為永遠無法擷取到。 
b. 兩個匿名對象永遠都不可能是同一個對象。 new 了兩次,位址肯定不一樣
  • 匿名對象好處:簡化書寫。  
  • 匿名對象的應用場景:  
a. 如果一個對象需要調用一個方法一次的時候,而調用完這個方法之後,該對象就不再使用了,這時候可以使用匿名對象。  new Student().study();
b. 可以作為實參傳入構造函數,裝飾者模式中經常用到;

是以,類可以看作是一個模闆,而對象則是類的一個執行個體(類看作是一張圖紙,對象則是按照圖紙生産的産品)。

1.2 抽象類和内部類

(1)抽象類

抽象類是不能執行個體化的類,用abstract關鍵字修飾class,其目的主要是代碼重用。除了不能執行個體化,形式上和一般的Java類并沒有太大差別,可以有一個或者多個抽象方法(沒有方法體,用abstract修飾),也可以沒有抽象方法。抽象類大多用于抽取相關Java類的共用方法實作或者是共同成員變量,然後通過繼承的方式達到代碼複用的目的。

  • 什麼時候用抽象類:

描述一類事物的時候,發現這類事物确實存在着某種行為,但是目前這種行為是不具體的,這時候應該抽取這種行為的聲明,而不去實作該種行為,這時候這種行為我們把它稱為抽象的行為,這時候應該使用抽象類。具體的行為在其子類中實作。

  • 抽象類要注意的細節
a. 如果一個方法沒有方法體,那麼該方法必須使用abstract修飾。    
b. 如果一個類含有抽象方法,那麼這個類肯定是一個抽象類或者接口。
c. 抽象類不能建立對象。
d. 抽象類是含有構造方法的。  
e. 抽象類可以存在非抽象方法與抽象方法。  
f. 抽象類可以不存在抽象方法。  
g. 非抽象類繼承抽象類的時候,必須要把抽象類中所有抽象方法全部實作。言外之意,抽象類繼承抽象類可以不全部實作。
  • 關于abstract的細節
a. abstract 不能與static共同修飾一個方法。static修飾的方法能被類直接調用,而abstract修飾的方法沒有方法體,有沖突。
b. abstract 不能與private共同修飾一個方法。 abstract修飾的方法,非抽象類繼承抽象類必須實作所有的抽象方法,私有不能被繼承
c. abstract不能以final關鍵字共同修飾一個方法。final 修飾的方法不能被重寫,這與抽象方法需被子類實作相違背。

(2)内部類

在類的内部定義的另一個類叫做内部類,使用内部類的好處:

  • 實作多重繼承;
/*我們知道,在java中一個類可以多重實作,但不能多重繼承。但有時候我們确實是需要實作多重繼承,
  例如:我們即繼承了父親的行為和特征也繼承了母親的行為和特征。
  那麼我們有沒有方法解決多重繼承的問題呢?内部内就提供了一種曲線實作多重繼承的方式
*/
    public class Father {
        public int strong(){
            return 9;//強壯指數
        }
    }
    public class Mother {
        public int kind(){
            return 8;//善良指數
        }
    }
    //子類通過内部類實作多重繼承
    public class Son {
        //繼承Father的内部類
        class FromFather extends Father{
            public int strong(){
                return super.strong() + 1;
            }
        }
        //繼承Mother的内部類
        class FromMother extends  Mother{
            public int kind(){
                return super.kind() - 2;
            }
        }
        public int getStrong(){
            return new FromFather().strong();
        }
        public int getKind(){
            return new FromMother().kind();
        }
    }
           
  • 内部類可以很好的實作隐藏:一般的非内部類,隻能用public和default,是不允許有 private 與protected權限的,但内部類可以;
  • 減少了類檔案編譯後的産生的位元組碼檔案的大小;(内部類在編譯完成後也會産生.class檔案,檔案名稱是:外部類名稱$内部類名稱.class)

使用内部類的缺點:程式結構不清楚

内部類又分為:成員内部類、局部内部類和匿名内部類。

定義 特點 注意事項 使用場景
成員(執行個體)内部類 在一個類的成員位置定義另外一個類,那麼另外一個類就稱作為成員内部類。

成員内部類的通路方式:

方式1: 在外部類内提供一個方法建立内部類的對象進行通路。

方式2: 在其他類建立内部類的對象進行通路。 建立的格式: 外部類.内部類  變量名 = new 外部類().new 内部類();

a. 成員内部類可以直接通路外部類成員(包括成員變量和成員方法)。

b. 如果成員内部類與外部類存在同名的成員,在内部類中預設是通路内部類的成員。成員通過“外部類.this.成員”指定通路外部類的成員。

c. 如果成員内部類出現了靜态的成員,那麼該成員内部類也必須使用static修飾。

d. 如果成員内部類是私有的,那麼建立内部類的對象就隻能在外部類提供方法建立。

每一個外部類對象都需要一個内部類的執行個體,内部類離不開外部類存在(相當于心髒對人體)。
局部内部類 在一個類的方法内部定義另外一個類,  另外一個類就稱作為局部内部類。 用在方法内部,作用範圍僅限于該方法中。在方法内部建立。

a. 如果局部内部類通路了局部變量,那麼該變量需要使用final修飾。

b. 不能使用private,protected,public修飾符

c.局部内部類可以直接通路外部類成員

如果内部類對象僅僅為外部類的某個方法使用,使用局部内部類。
匿名内部類 沒有類名的類,匿名内部類就是一種局部内部類

匿名内部類的格式:

 new 父類(父接口){

            匿名内部類的成員;

};

a.  必須存在繼承或者實作關系,預設繼承或實作new後面的類型

b. 匿名内部類沒有名字,是以沒有構造函數

簡化内部類的使用,在整個操作中隻使用一次的話

1.3 深入了解内部類

(1)為什麼内部類可以通路外部類的成員?

這篇博文通過反編譯内部類的位元組碼, 說明了内部類是如何通路外部類對象的成員的

關于内部類如何通路外部類的成員, 分析之後其實也很簡單, 主要是通過以下幾步做到的:

1 編譯器自動為内部類添加一個成員變量, 這個成員變量的類型和外部類的類型相同, 這個成員變量就是指向外部類對象的引用;(非靜态内部類對象有着指向其外部類對象的引用)

2 編譯器自動為内部類的構造方法添加一個參數, 參數的類型是外部類的類型, 在構造方法内部使用這個參數為1中添加的成員變量指派;

3 在調用内部類的構造函數初始化内部類對象時, 會預設傳入外部類的引用。

                                                              ——引自 https://blog.csdn.net/weixin_39214481/article/details/80372676

(2)為什麼局部内部類(包括匿名内部類)通路了局部函數的形參,該變量需要使用final修飾?

  • 首先要明确:成員函數的形參是也局部變量。
  • 沒有通路的局部函數的形參,是不用修飾的。
  • 并不是所有的局部變量都需要被final修飾,隻是被内部類引用的局部函數形參才需要被final修飾。

内部類并不是直接調用方法傳進來的參數,而是内部類将傳進來的參數通過自己的構造器備份到了自己的内部,自己内部的方法調用的實際是自己的屬性而不是外部類方法的參數。  

這樣了解就很容易得出為什麼要用final了,因為兩者從外表看起來是同一個東西,實際上卻不是這樣,如果内部類改掉了這些參數的值也不可能影響到原參數,然而這樣卻失去了參數的一緻性,因為從程式設計人員的角度來看他們是同一個東西,如果程式設計人員在程式設計的時候在内部類中改掉參數的值,但是外部調用的時候又發現值其實沒有被改掉,這就讓人非常的難以了解和接受,為了避免這種尴尬的問題存在,是以編譯器設計人員把内部類能夠使用的參數設定為必須是final來規避這種莫名其妙錯誤的存在。”

                                                                                   ——引自:https://www.cnblogs.com/jlustone/p/7517323.html

2. 類的生命周期

一個java的源檔案,經過編譯後生成字尾名為.class的檔案,即位元組碼檔案,java虛拟機就識别這種檔案,Java程式的生命周期就是class檔案從加載到消亡的過程。

深入類和對象——深入Java基礎系列(二)前言1. 概述2. 類的生命周期3. 對象的生命周期

魚骨圖中從尾巴開始,類的生命周期分為加載、連接配接、初始化、使用和解除安裝五大過程。

2.1 加載

位元組碼檔案并不是本地的可執行程式,當運作Java程式時,首先運作JVM,然後再把位元組碼檔案加載到JVM中運作。

類的加載過程其實就是将位元組碼檔案中的二進制資料讀入到記憶體中,首先Java虛拟機找到需要加載的class檔案,并把類的資訊加載到“運作時資料區”的方法區中,然後在堆區中(堆在JVM啟動時就建立)執行個體化一個java.lang.Class對象,用來封裝類在方法區内的資料結構,并作為方法區中這個類的資訊的入口(每個類都是Class類的執行個體)。

(1)類加載器

類的加載工作由類加載器來完成,它負責讀取.class檔案并轉換成java.lang.Class類的一個執行個體,加載到記憶體中,JVM啟動時,會形成由三個類加載器組成的初始類加載器層次結構。

  • BootStrap(啟動類加載器): 加載jdk/jre/lib/rt.jar(開發的時候使用的核心jar包);
  • ExtClassLoader(擴充類加載器):加載jdk/jre/lib/ext/*.jar(擴充包);
  • AppClassLoader(應用程式加載器):加載CLASSPATH中的jar包和class檔案;
深入類和對象——深入Java基礎系列(二)前言1. 概述2. 類的生命周期3. 對象的生命周期
public abstract class ClassLoader
public class SecureClassLoader extends ClassLoader
public class URLClassLoader extends SecureClassLoader
//AppClassLoader和ExtClassLoader都繼承于URLClassLoader
static class AppClassLoader extends URLClassLoader
static class ExtClassLoader extends URLClassLoader
           

除了啟動類加載器Bootstrap ClassLoader,其他的類加載器都是ClassLoader的子類。Bootstrap ClassLoader使用C++寫的。

Application ClassLoader的Parent是Extension ClassLoader,而Extension ClassLoader的Parent為Bootstrap ClassLoader。加載一個類時,首先BootStrap進行尋找,找不到再由Extension ClassLoader尋找,最後才是Application ClassLoader。(這裡的Parent并不是繼承體系,而是委派體系)

(2)類加載器特征

  • 全盤負責

一個類A是由一個類加載器加載的,如果A中關聯(繼承或包含)到其他的非系統類,那麼類B也是由該類加載器加載

  • 類加載器雙親委托加載

機制:如果一個類加載器收到一個類加載請求,該加載器不會自己去嘗試加載這個類,而是把這個請求轉交給父類加載器,先委托其父類加載器,如果還有父類加載器就繼續委托,直到沒有父類加載器為止,最頂層的類加載器(啟動類加載器)就需要真正的去加載指定類,如果在其類目錄中找不到這個類,繼續往下找,找到發起者類加載器為止,若找不到則報ClassNotFound錯誤。

雙親委派的好處:防止有些類被重複加載,有效確定一個類的全局唯一性,當程式中出現多個限定名相同的類時,類加載器在執行加載時,始終隻會加載其中的某一個類。

2.2 連接配接

一般會跟加載階段和初始化階段交叉進行,這個階段的主要任務就是做一些加載後的驗證工作以及一些初始化前的準備工作,過程由三部分組成,即驗證、準備和解析三步:

(1)驗證

當類被加載之後,必須要驗證一下這個類是否合法,比如該類是否符合位元組碼格式規範,變量與方法是不是有重複、資料類型是不是有效,繼承與實作是否合乎标準等等。總之,這個階段的目的就是保證加載的類是能夠被jvm所運作。

(2)準備

準備階段的工作就是為類的靜态變量配置設定記憶體并設為jvm預設的初值(在方法區配置設定),對于非靜态的變量,則不會為它們配置設定記憶體(執行個體化變量在對象執行個體化步驟中配置設定記憶體)。有一點需要注意,這時候,靜态變量的初值為jvm預設的初值,而不是我們在程式中設定的初值。jvm預設的初值如下:

  • 八種基本資料類型預設的初始值是0
  • 引用類型預設的初始值是null
  • 有static final修飾的會直接指派,例如:static final int x=10;則預設就是10。

(3)解析

這一階段的任務就是把常量池中的符号引用轉換為直接引用,就是jvm會将所有的類或接口名、字段名、方法名轉換為具體的記憶體位址(通過記憶體位址才能直接找到符号指向的内容)。

2.3 初始化

這個階段就是将靜态變量(類變量)指派的過程,即隻有static修飾的才能被初始化,執行的順序就是:父類靜态域或靜态代碼塊,然後是子類靜态域或者子類靜态代碼塊;(在一個類中的靜态内容則按照順序執行)

2.4 使用

在類的使用過程中依然存在三步:對象執行個體化、垃圾收集、對象終結。這三個過程也就是對象的生命周期。

(1)對象執行個體化

  • 在堆區配置設定對象需要的記憶體

  配置設定的記憶體包括本類和父類的所有執行個體變量,但不包括任何靜态變量

  • 對所有執行個體變量賦預設值

  将方法區内對執行個體變量的定義拷貝一份到堆區,然後賦預設值

  • 執行執行個體初始化代碼

  初始化順序是先初始化父類再初始化子類,初始化時先執行執行個體代碼塊(非靜态語句塊)然後是構造方法

  • 如果有類似于Child c = new Child()形式的c引用的話,在棧區定義Child類型引用變量c,然後将堆區對象的位址指派給它

(2)垃圾收集

當對象不再被引用的時候,就會被虛拟機标上特别的垃圾記号,在堆中等待GC回收

(3)對象的終結:對象被GC回收後,對象就不再存在,對象的生命也就走到了盡頭

注意:靜态變量和靜态代碼塊是在初始化過程中指派和執行的,是以它是優先于非靜态成員存在于記憶體中(在new之前,靜态變量就已經被指派,靜态代碼塊就被執行了),當父類和子類的所有靜态内容執行完了之後,再執行父類非靜态代碼塊和構造函數,最後執行子類的非靜态代碼塊和構造函數。

2.5 類解除安裝

即類的生命周期走到了最後一步,程式中不再有該類的引用,也就是說類所會被JV對應的Class對象沒有被引用的時候,JVM就會執行垃圾回收,從此生命結束。

3. 對象的生命周期

清楚了類的生命周期,而對象的生命周期則在類的生命周期之中,也就是類的使用階段。下面兩篇博文對相關内容分析的非常到位,值得反複閱讀:

了解Java類加載器(一):Java類加載原了解析 https://blog.csdn.net/justloveyou_/article/details/72217806
深入了解Java對象的建立過程:類的初始化與執行個體化 https://blog.csdn.net/justloveyou_/article/details/72466416

從上面博文中注意幾個問題:

(1)對象建立的過程:

當一個對象被建立時,虛拟機就會為其配置設定記憶體來存放對象自己的執行個體變量及其從父類繼承過來的執行個體變量(即使這些從超類繼承過來的執行個體變量有可能被隐藏也會被配置設定空間)。在為這些執行個體變量配置設定記憶體的同時,這些執行個體變量也會被賦予預設值(零值)。在記憶體配置設定完成之後,Java虛拟機就會開始對新建立的對象按照程式猿的意志進行初始化。

在Java對象初始化過程中,主要涉及三種執行對象初始化的結構,分别是 執行個體變量初始化、執行個體代碼塊初始化 以及 構造函數初始化。

總的來說,類執行個體化的一般過程是:父類的類構造器<clinit>() -> 子類的類構造器<clinit>() -> 父類的成員變量和執行個體代碼塊 -> 父類的構造函數 -> 子類的成員變量和執行個體代碼塊 -> 子類的構造函數。

繼續閱讀