java 例項變數初始化

Coding-lover發表於2015-11-02

該題目源自微信公眾號(程式設計師的那些事)的推送:攜程 Java 工程師的一道物件導向面試題

題目是這樣的:求下面程式的輸出:

<code class="language-java hljs  has-numbering"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Base</span>
{</span>
   <span class="hljs-keyword">private</span> String baseName = <span class="hljs-string">"base"</span>;
   <span class="hljs-keyword">public</span> <span class="hljs-title">Base</span>()
   {
       callName();
   }
   <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">callName</span>()
   {
       System. out. println(baseName);
   }
   <span class="hljs-keyword">static</span> class Sub extends Base
   {
       <span class="hljs-keyword">private</span> String baseName = <span class="hljs-string">"sub"</span>;
       <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">callName</span>()
       {
           System. out. println (baseName) ;
       }
   }
   <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span>(String[] args)
   {
       Base b = <span class="hljs-keyword">new</span> Sub();
   }
}</code><ul class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li></ul>

很顯然一開始我也做錯了,原因很簡單,這道題目考察什麼我大概知道,可是之前在學習類的載入機制時對類的初始化過程還比較瞭解,但例項變數的初始化過程比較模糊。這裡還牽扯的難點有:繼承的時候子類的同名屬性不會覆蓋父類,會將父類的屬性隱藏;在父類的建構函式裡面呼叫虛擬函式引起多型的變態程式碼。

接下來檢視了一下深入Java虛擬機器(Bill Venners著) 等才對該問題有了清楚的認識。下面我會盡量簡單而清晰的將其分析的透徹明瞭。

前面我在JAVA類載入和初始化 中簡單的講解了類的載入連結初始化過程,有興趣的可以看看,裡面也有一道變態的題目。

類的例項變數初始化的過程
一旦一個類被載入連線初始化,他就可以隨時被使用了,程式可以訪問他的靜態欄位,呼叫靜態方法,或者建立它的例項。在Java程式中類可以被明確或者隱含地例項化,有四種途徑:明確使用new操作符;呼叫Class或者Constructor物件的newInstance()方法;呼叫任何現有物件的clone()方法;或者通過objectInputStream類的getObject()方法反序列化。

當虛擬機器建立一個新的例項時,都需要在堆中為儲存物件的例項分配記憶體。所有在物件的類中和它的超類中宣告的變數(包括隱藏的例項變數)都要分配記憶體。一旦虛擬機器為新的物件準備好堆記憶體,它立即把例項變數初始化為預設的初始值。這一點很類似於類變數在連結的準備階段賦予預設初始值是一樣樣的。

一旦虛擬機器完成了為新的物件分配記憶體和為例項變數初始化為默賦予正確認的初始值後,接下來就會為例項變數的初始值。即呼叫物件的例項初始化方法,在java的class檔案中稱之為< init >()方法,類似於類初始化的< clinit >()方法。

一個< init >()方法可能包含三種程式碼:

  • 呼叫另一個< init >()方法
  • 實現對任何例項變數的初始化
  • 構造方法體的程式碼

實際上,一般有下面的情況:

  • 建構函式明確的呼叫了同一個類中的另一個構造方法,即呼叫了this(),它對應的< init >()方法由兩部分構成:
    一個同類的< init >(…)方法的呼叫
    實現了對應構造方法的方法體的位元組碼

  • 不是以this()開始的,也不是Object,由三部分組成:
    超類的< init >()方法呼叫
    任意例項變數初始化方法的位元組碼
    實現了對應構造方法的方法體的位元組碼

什麼意思?簡單理解就是說< init >()就是從class檔案位元組碼角度的建構函式,一般由Java程式碼裡面的幾部分構成。

超類的< init >()方法呼叫———————>對應super()
任意例項變數初始化方法的位元組碼————>對應定義變數時的賦值程式碼
實現了對應構造方法的方法體的位元組碼——>建構函式裡面的程式碼

注:Java保證了一個物件被初始化前其父類也必須被初始化。有下面機制來保證:Java強制要求任何類的建構函式中的第一句必須是呼叫父類建構函式或者是類中定義的其他建構函式。如果沒有建構函式,系統新增預設的無參建構函式,如果我們的建構函式中沒有顯示的呼叫父類的建構函式,那麼編譯器自動生成一個父類的無參建構函式。

舉個例子:

<code class="language-java hljs  has-numbering">class B {
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> b = <span class="hljs-number">10</span>;
  <span class="hljs-keyword">public</span> <span class="hljs-title">B</span>(){
    b = <span class="hljs-number">100</span>;
  }
}</code><ul class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li></ul>

編譯成class檔案後,使用命令 javap -c B.class 反編譯
反編譯B.class

很顯然可以看到初始化< init >()分為三部分

<code class="language-text hljs lasso has-numbering"><span class="hljs-comment">// 第一部分:父類的<init>()方法</span>
<span class="hljs-number">0</span>: aload_0
<span class="hljs-number">1</span>: invokespecial <span class="hljs-variable">#1</span>                  <span class="hljs-comment">// Method java/lang/Object."<init>":()V</span>
<span class="hljs-comment">// 第二部分:例項變數初始化,也就是定義變數時的賦值</span>
<span class="hljs-number">4</span>: aload_0
<span class="hljs-number">5</span>: bipush        <span class="hljs-number">10</span>
<span class="hljs-number">7</span>: putfield      <span class="hljs-variable">#2</span>                  <span class="hljs-comment">// Field b:I</span>
<span class="hljs-comment">// 第三部分:建構函式方法體</span>
<span class="hljs-number">10</span>: aload_0
<span class="hljs-number">11</span>: bipush        <span class="hljs-number">100</span>
<span class="hljs-number">13</span>: putfield      <span class="hljs-variable">#2</span>                  <span class="hljs-comment">// Field b:I</span>
<span class="hljs-number">16</span>: <span class="hljs-keyword">return</span></code><ul class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li></ul>

學習到這裡總結一下前面學習的內容就是:
Java例項變數在初始化時的順序是父類的初始化程式碼(xxx—>xxx—>xxx)—>定義變數時直接賦值—>建構函式程式碼塊。

回頭看開始的問題:
直到Base類和Sub類完成了初始化過程(類初始化完成)
在初始化Sub物件前,首先在堆區開闢記憶體並將子類中的baseName和父類中的baseName(已被隱藏)均賦為null
接下來執行物件的初始化過程,由於Sub類的建構函式沒有寫,初始化程式碼包含三部分:

  1. super();呼叫Base類的init<>()

    1. 呼叫super()也就是Object類的init<>()
    2. baseName = “base”;這裡是父類的baseName賦值。
    3. 父類建構函式裡面的:呼叫callName()由於該函式是在Sub類的裡面呼叫的,所以當前的this其實是子類,由於多型呼叫子類Sub的callName方法此時子類的baseName變數還未賦值還是null!
  2. baseName = “sub”;這裡是子類的baseName賦值。

  3. 空(建構函式什麼都沒有)

所以輸出null!


轉載自:由一道面試題所想到的--Java例項變數初始化

相關文章