從一次實際經歷說說Java類初始化順序中容易忽略的點 1. 說說坑? 2. 聊聊Kotlin?

  我們從第一天學習Java開始,就對Java的類初始化順序牢記於心。但是在實際開發過程中,似乎很難能接觸這一部分的應用。在這之前,我也認爲它只是面試中八股文而已,直到最近踩了一個坑,這才發現它是多麼的重要。

1. 說說坑?

  地球人都知道,在Java世界中,類初始化順序是:父類的靜態成員變量或者靜態代碼塊-> 子類的靜態成員變量或者靜態-> 父類成員變量或者普通代碼塊 ->父類的構造方法-> 子類成員變量或者普通代碼塊 -> 子類的構造方法
  我們在理解上都沒有問題,但是使用上很容易忽略這個順序。我舉一個例子:假設有兩個類,分別是AnimalDog類,其中Dog繼承於Animal(是不是感覺回到了學習Java的第一天?)。它們的代碼如下:

public class Animal {
    public Animal() {
        eat();
    }

    protected void eat() {
    }
}

// ----------------------------
public class Dog extends Animal {

    private String foodName = "肉";

    public Dog() {
    }

    @Override
    protected void eat() {
        System.out.println("我喫" + foodName);
    }
}

  代碼實現很簡單,總的來說就是:父類有一個非final的方法在其構造方法裏面回調,子類重寫了這個方法,同時在這方法裏面訪問了一個自己的一個成員變量。
  那麼這裏會什麼問題呢?大家可以運行一下上面的代碼,發現輸出的結果是:我喫null,也就是說foodName還沒有初始化。在這裏,很容易理解爲什麼foodName沒有初始化,因爲在類初始化順序中,子類的成員變量在父類的構造方法之後才初始化。這個Demo足夠的簡單,所以大家覺得沒有問題,在正式的開發環境中,情況一般都是非常複雜的,代碼成千上萬,我們在一個方法裏面訪問本類的成員變量,一般不會關心這個方法是什麼時候回調的,從而導致在開發過程中遇到不可預知的結果。
  不過幸運的是,這種問題是必現的,一般在開發中就能遇到並且解決,並不會帶到線上去,但是我還是想說:父類的構造方法(代碼塊)裏面不要調用可以被子類重寫的方法
  除此以外,這種調用方式還會遇到一個隱式的問題:

public class Dog extends Animal {
    
    private String foodName1;
    private String foodName2 = null;

    public Dog() {
    }

    @Override
    protected void eat() {
        foodName1 = "肉";
        foodName2 = "骨頭";
    }

    public void print() {
        System.out.println("foodName1 = " + foodName1 + " foodName2 = " + foodName2);
    }
}

  我改寫了Dog類的代碼,在其內部定義了兩個成員變量,分別是:foodName1foodName2,它們都在eat方法裏面賦值,需要注意的是,foodName2在定義設置默認值爲null,而foodName1沒有設置默認值。那麼我們創建Dog的對象,然後調用它的print方法,結果如何呢?結果是:foodName1 = 肉 foodName2 = null。也就是說,設置默認值的成員變量即使在方法裏面重新賦值,在真正使用的時候卻還是默認值。這是爲啥呢?其實要想真正瞭解其內部原理,就要扒Dog類的字節碼了,但是我懶得扒,猜測其原因是:如果在定義成員變量沒有設置默認值時,當其初始化時發現已經被初始化,就不再進行初始化。換言之,如果設置了默認值,即使之前已經被賦值,也會被默認值覆蓋。
  相比於第一個坑,第二個坑顯得更加隱式和不容易理解。總之還是那麼一句話:父類的構造方法(代碼塊)裏面不要調用可以被子類重寫的方法。最重要的是,在這個過程中,編譯器並沒有做出任何提示,這也是這類問題容易出現的原因之一吧。

2. 聊聊Kotlin?

  也許大家在Java中習慣了上面的使用方式,同時編譯器也沒有報錯,說明官方也默認這種操作(我亂說的)。但是,我們以相同的方式應用在Kotlin中,編譯器報了一個警告:

open class Test {
    private val string = getString()

    protected open fun getString(): String {
        return ""
    }
}

  總體上來說,比Java友好的多,至少有了一個警告提示。
  從這裏,我們可以得出來兩個結論:

  1. 不要隨意忽略編譯器給出的警告。
  2. 快去使用Kotlin吧!!!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章