天天看點

Java重寫方法與初始化的隐患(轉)前言問題 實際分析理論解釋題外話

Java重寫方法與初始化的隐患(轉)前言問題 實際分析理論解釋題外話

如果你已經知道我要說什麼了, 可以鄙視我.

簡單還原一下問題, 我們有一個類SuperClass

1

2

3

4

5

6

7

8

9

10

11

12

<code>public</code> <code>class</code> <code>SuperClass {</code>

<code>    </code><code>private</code> <code>int</code> <code>mSuperX;</code>

<code>    </code><code>public</code> <code>SuperClass() {</code>

<code>        </code><code>setX(</code><code>99</code><code>);</code>

<code>    </code><code>}</code>

<code>    </code><code>public</code> <code>void</code> <code>setX(</code><code>int</code> <code>x) {</code>

<code>        </code><code>mSuperX = x;</code>

<code>}</code>

現在我們想随時知道<code>mSuperX</code>是什麼值, 不用反射, 因為父類從不直接修改mSuperX的值, 總是通過<code>setX</code>來改, 那麼最簡單的方法就是繼承SuperClass, 重寫setX方法, 監聽它的改變就好.下面是我們的子類SubClass:

13

14

15

16

17

<code>public</code> <code>class</code> <code>SubClass</code><code>extends</code> <code>SuperClass {</code>

<code>    </code><code>private</code> <code>int</code> <code>mSubX =</code><code>1</code><code>;</code>

<code>    </code><code>public</code> <code>SubClass() {}</code>

<code>    </code><code>@Override</code>

<code>        </code><code>super</code><code>.setX(x);</code>

<code>        </code><code>mSubX = x;</code>

<code>        </code><code>System.out.println(</code><code>"SubX is assigned "</code> <code>+ x);</code>

<code>    </code><code>public</code> <code>void</code> <code>printX() {</code>

<code>        </code><code>System.out.println(</code><code>"SubX = "</code> <code>+ mSubX);</code>

我使用<code>mSubX</code>來跟蹤<code>mSuperX</code>

因為在ViewGroup中, clipToPadding預設值是true(為了簡化問題, 把它當成boolean, 實際并不是), 而ViewGroup初始化有可能不調用setClipToPadding, 此時是預設值, 為了模拟這種情況, 将mSubX初始化為1.

最後在main裡調用:

<code>public</code> <code>class</code> <code>Main {</code>

<code>    </code><code>public</code> <code>static</code> <code>void</code> <code>main(String[] args) {</code>

<code>        </code><code>SubClass sc =</code><code>new</code> <code>SubClass();</code>

<code>        </code><code>sc.printX();</code>

很多人, 包括我, 認為終端輸出的結果應該是:

<code>SubX is assigned</code><code>99</code>

<code>SubX =</code><code>99</code>

然而真正運作後輸出的是:

<code>SubX =</code><code>1</code>

Java重寫方法與初始化的隐患(轉)前言問題 實際分析理論解釋題外話

要想知道發生了什麼, 最簡單的方法就是看看到底程式到底是怎麼執行的, 比如單步調試, 或者直接一點, 看看Java位元組碼.

下面是Main的位元組碼

<code>Compiled from</code><code>"Main.java"</code>

<code>public</code> <code>class</code> <code>bugme.Main {</code>

<code>  </code><code>......</code>

<code>  </code><code>public</code> <code>static</code> <code>void</code> <code>main(java.lang.String[]);</code>

<code>    </code><code>Code:</code>

<code>       </code><code>0</code><code>:</code><code>new</code>           <code>#</code><code>2</code>                  <code>// class bugme/SubClass</code>

<code>       </code><code>3</code><code>: dup          </code>

<code>       </code><code>4</code><code>: invokespecial #</code><code>3</code>                  <code>// Method bugme/SubClass."&lt;init&gt;":()V</code>

<code>       </code><code>...... </code>

這是直接用javap反編譯.class檔案得到的. 雖說同樣是Java寫的, 用apktool反編譯APK檔案(其中的dex檔案)得到的smali代碼和Java Bytecode明顯長得不一樣.

位元組碼乍一看怪怪的, 隻要知道它隐含了一個棧和局部變量表就好懂了.

這段代碼首先<code>new</code>一個SubClass執行個體, 把引用入棧, <code>dup</code>是把棧頂複制一份入棧, <code>invokespecial #3</code>将棧頂元素出棧并調用它的某個方法, 這個方法具體是什麼要看常量池裡第3個條目是什麼, 但是javap生成的位元組碼直接給我們寫在旁邊了, 即<code>SubClass.&lt;init&gt;</code>.

接下來看<code>SubClass.&lt;init&gt;</code>,

<code>public</code> <code>class</code> <code>bugme.SubClass</code><code>extends</code> <code>bugme.SuperClass {</code>

<code>  </code><code>public</code> <code>bugme.SubClass();</code>

<code>       </code><code>0</code><code>: aload_0      </code>

<code>       </code><code>1</code><code>: invokespecial #</code><code>1</code>                  <code>// Method bugme/SuperClass."&lt;init&gt;":()V</code>

<code>       </code><code>......</code>

這裡面并沒有方法叫<code>&lt;init&gt;</code>, 是因為javap為了友善我們閱讀, 直接把它改成類名<code>bugme.SubClass</code>, 順便一提, bugme是包名. <code>&lt;init&gt;</code>方法并非通常意義上的構造方法, 這是Java幫我們合成的一個方法, 裡面的指令會幫我們按順序進行普通成員變量初始化, 也包括初始化塊裡的代碼, 注意是按順序執行, 這些都執行完了之後才輪到構造方法裡代碼生成的指令執行. 這裡<code>aload_0</code>将局部變量表中下标為0的元素入棧, 其實就是Java中的<code>this</code>, 結合<code>invokespecial #1</code>, 是在調用父類的構造函數, 也就是我們常見的super().

是以我們再看<code>SuperClass.&lt;init&gt;</code>

<code>public</code> <code>class</code> <code>bugme.SuperClass {</code>

<code>  </code><code>public</code> <code>bugme.SuperClass();</code>

<code>       </code><code>1</code><code>: invokespecial #</code><code>1</code>                  <code>// Method java/lang/Object."&lt;init&gt;":()V</code>

<code>       </code><code>4</code><code>: aload_0      </code>

<code>       </code><code>5</code><code>: bipush       </code><code>99</code>

<code>       </code><code>7</code><code>: invokevirtual #</code><code>2</code>                  <code>// Method setX:(I)V</code>

<code>      </code><code>10</code><code>:</code><code>return</code> 

<code>  </code><code>......    </code>

同樣是先調了父類<code>Object</code>的構造方法, 然後再将<code>this</code>, <code>99</code>入棧, <code>invokevirtual #2</code>旁邊注釋了是調用<code>setX</code>, 參數分别是<code>this</code>和<code>99</code>也就是<code>this.setX(99)</code>, 然而這個方法被重寫了, 調用的是子類的方法, 是以我們再看<code>SubClass.setX</code>:

<code>  </code><code>public</code> <code>void</code> <code>setX(</code><code>int</code><code>);</code>

<code>       </code><code>1</code><code>: iload_1      </code>

<code>       </code><code>2</code><code>: invokespecial #</code><code>3</code>                  <code>// Method bugme/SuperClass.setX:(I)V</code>

這裡将局部變量表前兩個元素都入棧, 第一個是<code>this</code>, 第二個是括号裡的參數, 也就是<code>99</code>,<code>invokespecial #3</code>調用的是父類的<code>setX</code>, 也就是我們代碼中寫的<code>super.setX(int)</code>

<code>SuperClass.setX</code>就很簡單了:

<code>       </code><code>2</code><code>: putfield      #</code><code>3</code>                  <code>// Field mSuperX:I</code>

<code>       </code><code>5</code><code>:</code><code>return</code>       

這裡先把<code>this</code>入棧, 再把參數入棧, <code>putfield #3</code>使得前兩個入棧的元素全部出棧, 而成員<code>mSuperX</code>被指派, 這四條指令隻對應代碼裡的一句<code>this.mSuperX = x;</code>

接下來控制流回到子類的<code>setX</code>:

18

19

20

21

22

<code>     </code><code>-&gt;</code><code>5</code><code>: aload_0                          </code><code>// 即将執行這句</code>

<code>       </code><code>6</code><code>: iload_1      </code>

<code>       </code><code>7</code><code>: putfield      #</code><code>2</code>                  <code>// Field mSubX:I</code>

<code>      </code><code>10</code><code>: getstatic     #</code><code>4</code>                  <code>// Field java/lang/System.out:Ljava/io/PrintStream;</code>

<code>      </code><code>13</code><code>:</code><code>new</code>           <code>#</code><code>5</code>                  <code>// class java/lang/StringBuilder</code>

<code>      </code><code>16</code><code>: dup          </code>

<code>      </code><code>17</code><code>: invokespecial #</code><code>6</code>                  <code>// Method java/lang/StringBuilder."&lt;init&gt;":()V</code>

<code>      </code><code>20</code><code>: ldc           #</code><code>7</code>                  <code>// String SubX is assigned</code>

<code>      </code><code>22</code><code>: invokevirtual #</code><code>8</code>                  <code>// Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;</code>

<code>      </code><code>25</code><code>: iload_1      </code>

<code>      </code><code>26</code><code>: invokevirtual #</code><code>9</code>                  <code>// Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;</code>

<code>      </code><code>29</code><code>: invokevirtual #</code><code>10</code>                 <code>// Method java/lang/StringBuilder.toString:()Ljava/lang/String;</code>

<code>      </code><code>32</code><code>: invokevirtual #</code><code>11</code>                 <code>// Method java/io/PrintStream.println:(Ljava/lang/String;)V</code>

<code>      </code><code>35</code><code>:</code><code>return</code>

從5處開始繼續分析, 5,6,7将參數的值賦給<code>mSubX</code>, 此時mSubX是99了, 下面那一堆則是在執行<code>System.out.println("SubX is assigned " + x);</code>并傳回, 還可以看到Java自動幫我們使用<code>StringBuilder</code>優化字元串拼接, 就不分析了.

說了這麼多, 我們的代碼才剛把下面箭頭指着的這句執行完:

<code>     </code><code>-&gt;</code><code>1</code><code>: invokespecial #</code><code>1</code>                  <code>// Method bugme/SuperClass."&lt;init&gt;":()V</code>

<code>       </code><code>5</code><code>: iconst_1     </code>

<code>       </code><code>6</code><code>: putfield      #</code><code>2</code>                  <code>// Field mSubX:I</code>

<code>       </code><code>9</code><code>:</code><code>return</code>       

<code>  </code><code>......     </code>

此時<code>mSubX</code>已經是99了, 再執行下面的4,5,6, 這一部分是<code>SubClass</code>的初始化, 代碼将把<code>1</code>賦給<code>mSubX</code>,99被1覆寫了.

方法傳回後, 相當于我們執行完了箭頭指的這一句代碼:

<code>      </code><code>-&gt;SubClass sc =</code><code>new</code> <code>SubClass();</code>

接下來執行的代碼将列印<code>mSubX</code>的值, 自然就是1了.

我們都知道Java是面向對象的語言, 面向對象三大特性之一多态性. 假如父類構造方法中調用了某個方法, 這個方法恰好被子類重寫了, 會發生什麼?

根據多态性, 實際被調用的是子類的方法, 這個沒錯. 再考慮有繼承時, 初始化的順序. 如果是new一個子類, 那麼初始化順序是:

父類static成員 -&gt; 子類static成員 -&gt; 父類普通成員初始化和初始化塊 -&gt; 父類構造方法 -&gt; 子類普通成員初始化和初始化塊 -&gt; 子類構造方法

父類構造方法中調用了一次<code>setX</code>, 此時<code>mSubX</code>中已經是我們要跟蹤的值, 但之後子類普通成員初始化将<code>mSubX</code>又初始化了一遍, 覆寫了前面我們跟蹤的值, 自然得到的值就是錯的.

Java中, 在構造方法中唯一能安全調用的是基類中的final方法, 自己的final方法(自己的private方法自動final), 如果類本身是final的, 自然就能安全調用自己所有的方法.

完全遵守這個準則, 可以保證不會出這個bug. 實際上我們常常不能遵守, 是以要時刻小心這個問題.

這篇文章所有的知識點基本都是很基礎的, 我自己也都記得, 但當這些知識合在一起的時候, 他們之間産生的反應卻是我沒有注意過的. 這也是我寫這篇文章的原因.

如果以後有人面試拿這個問題考你, 你可能是遇上drakeet了.

關于預設初始化, 比如這樣寫:

<code>    </code><code>private</code> <code>int</code> <code>mSubX;</code>

<code>    </code><code>......</code>

如果父類保證一定會在初始化時調用<code>setX</code>, 程式是不會出現上面說的bug的, 因為預設初始化并不是靠生成下面這樣的代碼預設初始化.

<code>4</code><code>: aload_0      </code>

<code>5</code><code>: iconst_1     </code>

<code>6</code><code>: putfield      #</code><code>2</code>                  <code>// Field mSubX:I</code>

所謂的預設初始化, 其實是我們要執行個體化一個對象之前, 需要一塊記憶體放我們的資料, 這塊記憶體被全部置為0, 這就是預設初始化了.

下面這兩句話, 雖然效果一樣, 但實際是有差別的.

<code>private</code> <code>int</code> <code>mSubX;</code>

<code>private</code> <code>int</code> <code>mSubX =</code><code>0</code><code>;</code>

一般情況下, 這兩句代碼對程式沒有任何影響(除非你遇到這個bug), 上面一句和下面一句的差別在于, 下面一句會導緻<code>&lt;init&gt;</code>方法裡面生成3條指令, 分别是<code>aload_0</code>, <code>iconst_0</code>, <code>putfield #**</code>, 而上面一句則不會.

是以如果你的成員變量使用預設值初始化, 就沒必要自己賦那個預設值, 而且還能省3條指令.

http://www.importnew.com/17000.html

繼續閱讀