成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Java 重寫方法與初始化的隱患

開發(fā) 后端
所以如果你的成員變量使用默認值初始化, 就沒必要自己賦那個默認值, 而且還能省3條指令.

雖然文章標題是Java, 但幾乎所有面向?qū)ο笤O計的語言都遵守這個初始化流程, 感謝廖祜秋liaohuqiu_秋百萬指出, 之前忘記提這個了.

前言

drakeet寫了個和RecyclerView相關的GenerousRecyclerView, 原文提到了寫這個的目的. 因為需要知道ViewGroup的clipToPadding屬性, 所以調(diào)用了ViewGroup.getClipToPadding, 但這個方法是API level 21引入的. 我看了一下代碼, ViewGroup是通過調(diào)用setClipToPadding完成相關內(nèi)容初始化的, setClipToPadding在API level 1就有了, 也就是說我們只要監(jiān)視setClipToPadding的調(diào)用, 就能知道ViewGroup的clipToPadding狀態(tài). 如此巧妙, 如果我告訴drakeet, 說不定就能引起他的注意, 出任CEO, 走上人生***.

Java重寫方法與初始化的隱患

如果你已經(jīng)知道我要說什么了, 可以鄙視我.

問題

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

 

  1. public class SuperClass { 
  2.  
  3.     private int mSuperX; 
  4.  
  5.     public SuperClass() { 
  6.         setX(99); 
  7.     } 
  8.  
  9.     public void setX(int x) { 
  10.         mSuperX = x; 
  11.     } 

現(xiàn)在我們想隨時知道mSuperX是什么值, 不用反射, 因為父類從不直接修改mSuperX的值, 總是通過setX來改, 那么最簡單的方法就是繼承SuperClass, 重寫setX方法, 監(jiān)聽它的改變就好.下面是我們的子類SubClass:

  1. public class SubClass extends SuperClass { 
  2.  
  3.     private int mSubX = 1
  4.  
  5.     public SubClass() {} 
  6.  
  7.     @Override 
  8.     public void setX(int x) { 
  9.         super.setX(x); 
  10.         mSubX = x; 
  11.         System.out.println("SubX is assigned " + x); 
  12.     } 
  13.  
  14.     public void printX() { 
  15.         System.out.println("SubX = " + mSubX); 
  16.     } 

我使用mSubX來跟蹤mSuperX

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

***在main里調(diào)用:

  1. public class Main { 
  2.     public static void main(String[] args) { 
  3.         SubClass sc = new SubClass(); 
  4.         sc.printX(); 
  5.     } 

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

SubX is assigned 99
SubX = 99

然而真正運行后輸出的是:

  1. SubX is assigned 99 
  2. SubX = 1 

Java重寫方法與初始化的隱患

實際分析

要想知道發(fā)生了什么, 最簡單的方法就是看看到底程序到底是怎么執(zhí)行的, 比如單步調(diào)試, 或者直接一點, 看看Java字節(jié)碼.

下面是Main的字節(jié)碼

  1. Compiled from "Main.java" 
  2. public class bugme.Main { 
  3.   ...... 
  4.   public static void main(java.lang.String[]); 
  5.     Code: 
  6.        0new           #2                  // class bugme/SubClass 
  7.        3: dup           
  8.        4: invokespecial #3                  // Method bugme/SubClass."<init>":()V 
  9.        ......  

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

字節(jié)碼乍一看怪怪的, 只要知道它隱含了一個棧和局部變量表就好懂了.

這段代碼首先new一個SubClass實例, 把引用入棧, dup是把棧頂復制一份入棧, invokespecial #3將棧頂元素出棧并調(diào)用它的某個方法, 這個方法具體是什么要看常量池里第3個條目是什么, 但是javap生成的字節(jié)碼直接給我們寫在旁邊了, 即SubClass.<init>.

接下來看SubClass.<init>,

  1. public class bugme.SubClass extends bugme.SuperClass { 
  2.   public bugme.SubClass(); 
  3.     Code: 
  4.        0: aload_0       
  5.        1: invokespecial #1                  // Method bugme/SuperClass."<init>":()V 
  6.        ...... 

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

所以我們再看SuperClass.<init>

 

  1. public class bugme.SuperClass { 
  2.   public bugme.SuperClass(); 
  3.     Code: 
  4.        0: aload_0       
  5.        1: invokespecial #1                  // Method java/lang/Object."<init>":()V 
  6.        4: aload_0       
  7.        5: bipush        99 
  8.        7: invokevirtual #2                  // Method setX:(I)V 
  9.       10return  
  10.  
  11.   ......     

同樣是先調(diào)了父類Object的構造方法, 然后再將this, 99入棧, invokevirtual #2旁邊注釋了是調(diào)用setX, 參數(shù)分別是this99也就是this.setX(99), 然而這個方法被重寫了, 調(diào)用的是子類的方法, 所以我們再看SubClass.setX:

  1. public class bugme.SubClass extends bugme.SuperClass { 
  2.   ...... 
  3.   public void setX(int); 
  4.     Code: 
  5.        0: aload_0       
  6.        1: iload_1       
  7.        2: invokespecial #3                  // Method bugme/SuperClass.setX:(I)V 
  8.        ...... 

這里將局部變量表前兩個元素都入棧, ***個是this, 第二個是括號里的參數(shù), 也就是99, invokespecial #3調(diào)用的是父類的setX, 也就是我們代碼中寫的super.setX(int)

SuperClass.setX就很簡單了:

  1. public class bugme.SuperClass { 
  2.   ......     
  3.   public void setX(int); 
  4.     Code: 
  5.        0: aload_0       
  6.        1: iload_1       
  7.        2: putfield      #3                  // Field mSuperX:I 
  8.        5return        

這里先把this入棧, 再把參數(shù)入棧, putfield #3使得前兩個入棧的元素全部出棧, 而成員mSuperX被賦值, 這四條指令只對應代碼里的一句this.mSuperX = x;

接下來控制流回到子類的setX:

  1. public class bugme.SubClass extends bugme.SuperClass { 
  2.   ...... 
  3.   public void setX(int); 
  4.     Code: 
  5.        0: aload_0       
  6.        1: iload_1       
  7.        2: invokespecial #3                  // Method bugme/SuperClass.setX:(I)V 
  8.      ->5: aload_0                           // 即將執(zhí)行這句 
  9.        6: iload_1       
  10.        7: putfield      #2                  // Field mSubX:I 
  11.       10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream; 
  12.       13new           #5                  // class java/lang/StringBuilder 
  13.       16: dup           
  14.       17: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V 
  15.       20: ldc           #7                  // String SubX is assigned 
  16.       22: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 
  17.       25: iload_1       
  18.       26: invokevirtual #9                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 
  19.       29: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 
  20.       32: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 
  21.       35return 

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

說了這么多, 我們的代碼才剛把下面箭頭指著的這句執(zhí)行完:

 

  1. public class bugme.SubClass extends bugme.SuperClass { 
  2.   public bugme.SubClass(); 
  3.     Code: 
  4.        0: aload_0       
  5.      ->1: invokespecial #1                  // Method bugme/SuperClass."<init>":()V 
  6.        4: aload_0       
  7.        5: iconst_1      
  8.        6: putfield      #2                  // Field mSubX:I 
  9.        9return        
  10.  
  11.   ......      

此時mSubX已經(jīng)是99了, 再執(zhí)行下面的4,5,6, 這一部分是SubClass的初始化, 代碼將把1賦給mSubX, 99被1覆蓋了.

方法返回后, 相當于我們執(zhí)行完了箭頭指的這一句代碼:

  1. public class Main { 
  2.     public static void main(String[] args) { 
  3.       ->SubClass sc = new SubClass(); 
  4.         sc.printX(); 
  5.     } 

接下來執(zhí)行的代碼將打印mSubX的值, 自然就是1了.

以前就聽說過JVM是基于棧的, Dalvik是基于寄存器的, 現(xiàn)在看了Java字節(jié)碼, 回想一下smali, 自然就能明白. 我在Android無需權限顯示懸浮窗, 兼談逆向分析app中有分析smali代碼, smali里面經(jīng)??吹筋愃苬0, v1這類東西, 是在操作寄存器, 而剛才分析的bytecode, 指令常常伴隨著入棧出棧.

理論解釋

我們都知道Java是面向?qū)ο蟮恼Z言, 面向?qū)ο笕筇匦灾欢鄳B(tài)性. 假如父類構造方法中調(diào)用了某個方法, 這個方法恰好被子類重寫了, 會發(fā)生什么?

根據(jù)多態(tài)性, 實際被調(diào)用的是子類的方法, 這個沒錯. 再考慮有繼承時, 初始化的順序. 如果是new一個子類, 那么初始化順序是:

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

父類構造方法中調(diào)用了一次setX, 此時mSubX中已經(jīng)是我們要跟蹤的值, 但之后子類普通成員初始化將mSubX又初始化了一遍, 覆蓋了前面我們跟蹤的值, 自然得到的值就是錯的.

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

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

這個東西在Java編程思想(第四版) (機械工業(yè)出版社 2012年11月第1版) 的8.3.3小節(jié)有寫過, 但是這種東西除非自己遇到bug了, 基本看過不會有印象.

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

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

題外話

關于默認初始化, 比如這樣寫:

 

  1. public class SubClass extends SuperClass { 
  2.     private int mSubX; 
  3.  
  4.     public SubClass() {} 
  5.     ...... 

如果父類保證一定會在初始化時調(diào)用setX, 程序是不會出現(xiàn)上面說的bug的, 因為默認初始化并不是靠生成下面這樣的代碼默認初始化.

       4: aload_0      
       5: iconst_1     
       6: putfield      #2                  // Field mSubX:I

所謂的默認初始化, 其實是我們要實例化一個對象之前, 需要一塊內(nèi)存放我們的數(shù)據(jù), 這塊內(nèi)存被全部置為0, 這就是默認初始化了.

下面這兩句話, 雖然效果一樣, 但實際是有區(qū)別的.

private int mSubX;

private int mSubX = 0;

一般情況下, 這兩句代碼對程序沒有任何影響(除非你遇到這個bug), 上面一句和下面一句的區(qū)別在于, 下面一句會導致<init>方法里面生成3條指令, 分別是aload_0, iconst_0, putfield #**, 而上面一句則不會.

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

 

責任編輯:王雪燕 來源: Shawon
相關推薦

2012-05-23 12:46:53

JavaJava類

2020-12-03 09:50:52

容器IoC流程

2009-12-16 14:04:04

Ruby對象初始化

2012-03-13 13:38:42

Java

2019-11-04 13:50:36

Java數(shù)組編程語言

2024-08-14 17:21:34

2009-05-20 10:58:15

數(shù)據(jù)庫查詢初始化

2023-11-12 23:08:17

C++初始化

2009-06-11 13:26:16

Java數(shù)組聲明創(chuàng)建

2010-02-01 14:21:24

C++初始化列表

2012-02-28 10:04:09

Java

2011-03-16 10:52:20

2009-06-10 16:17:00

Netbeans JT初始化

2021-07-07 05:00:17

初始化源碼

2024-03-08 08:26:25

類的加載Class文件Java

2010-01-22 15:47:37

VB.NET初始化網(wǎng)格

2009-08-28 11:43:26

C#數(shù)組初始化

2009-09-08 09:48:34

LINQ初始化數(shù)組

2009-11-11 15:29:15

ADO初始化

2011-06-09 14:13:06

C++JAVA缺省初始化
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 日韩视频精品 | 国产性色视频 | 在线国产一区 | 亚洲福利在线观看 | 国产精品视频网 | 国产欧美精品一区二区色综合朱莉 | 毛片a级毛片免费播放100 | 欧美国产精品一区二区 | 日韩精品中文字幕一区二区三区 | 中文字幕一区二区三区四区五区 | 一区二区在线免费观看 | 亚洲日本成人 | 欧美日韩高清免费 | 欧美日韩国产一区二区三区 | 青青草av在线播放 | 国产精品自拍一区 | 伊人网伊人网 | 国产精品乱码一区二三区小蝌蚪 | 99reav | 日韩中文一区 | 午夜爽爽男女免费观看hd | 国产一区91精品张津瑜 | 国产伦精品一区二区三区四区视频 | 中文字幕在线看第二 | 日韩精品在线视频免费观看 | 国产精品久久久久久久久久三级 | 亚洲综合国产 | 自拍视频网 | 成人性视频免费网站 | 日日操天天射 | 免费黄色录像视频 | 成人欧美一区二区三区在线观看 | 中文字幕视频在线观看 | 精品国产一区二区三区久久久蜜月 | 欧美国产精品一区二区 | 国产精品久久久久久婷婷天堂 | 麻豆国产一区二区三区四区 | 午夜免费精品视频 | 国产精品亚洲一区 | 久久久免费少妇高潮毛片 | 精品国产乱码久久久久久丨区2区 |