java 例項變數初始化
該題目源自微信公眾號(程式設計師的那些事)的推送:攜程 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 反編譯
很顯然可以看到初始化< 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類的建構函式沒有寫,初始化程式碼包含三部分:
-
super();呼叫Base類的init<>()
- 呼叫super()也就是Object類的init<>()
- baseName = “base”;這裡是父類的baseName賦值。
- 父類建構函式裡面的:呼叫callName()由於該函式是在Sub類的裡面呼叫的,所以當前的this其實是子類,由於多型呼叫子類Sub的callName方法此時子類的baseName變數還未賦值還是null!
-
baseName = “sub”;這裡是子類的baseName賦值。
-
空(建構函式什麼都沒有)
所以輸出null!
相關文章
- ruby中的類例項變數和例項的例項變數變數
- 【Java貓說】例項變數與區域性變數Java變數
- 類變數的初始化時機總是處於例項變數的初始化時機之前!變數
- Java類初始化和例項化Java
- 可變引數例項
- 靜態變數和例項變數區別?變數
- java執行緒安全問題之靜態變數、例項變數、區域性變數Java執行緒變數
- Objective-C例項變數Object變數
- 健壯的例項變數 (Non Fragile ivars)和脆弱的例項變數(Fragile ivars)變數
- 淺談Java中的例項初始化器Java
- Java變數的宣告和初始化Java變數
- 初始化python類的例項時,私有變數的值與上一個例項的相同,問題定位Python變數
- iOS 靜變數static、全域性變數extern、區域性變數、例項變數iOS變數
- 來說說 Java 中的例項初始化器Java
- 來說說Java中的例項初始化器Java
- Python - 物件導向程式設計 - 類變數、例項變數/類屬性、例項屬性Python物件程式設計變數
- JNI/NDK開發指南(7):C/C++訪問Java例項變數和靜態變數C++Java變數
- 成員變數、全域性變數、例項變數、類變數、靜態變數和區域性變數的區別變數
- SQL使用繫結變數,測試例項。SQL變數
- 關於例項變數和靜態變數的一點疑問變數
- hive 初始化變數Hive變數
- java_隨機數(統計例項)Java隨機
- C++ 結構體例項和類例項的初始化C++結構體
- java中靜態初始化塊,例項初始化塊,建構函式區別Java函式
- 執行緒問題2(注意例項變數)執行緒變數
- shell study-3day--shell變數及例項3D變數
- 關於JS中變數的作用域-例項JS變數
- 2 Day DBA-管理Oracle例項-關於初始化引數Oracle
- 淺談Java類中的變數初始化順序Java變數
- Java初始化靜態變數的時間順序Java變數
- 開發日記(一)JAVA中變數初始化流程Java變數
- js判斷變數是不是數字型別程式碼例項JS變數型別
- Java經典例項:比較浮點數Java
- 如何保證一個類中的例項變數不被改變變數
- Java例項教程Java
- java介面例項Java
- 探討Java類中成員變數的初始化方式Java變數
- css樣式初始化程式碼例項CSS