天天看点

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

继续阅读