Java 重写方法与初始化的隐患

虽然文章标题是Java, 但几乎所有面向对象设计的语言都遵守这个初始化流程, 感谢廖祜秋liaohuqiu_秋百万指出, 之前忘记提这个了.

前言

drakeet写了个和RecyclerView相关的GenerousRecyclerView, 原文提到了写这个的目的. 因为需要知道ViewGroup的clipToPadding属性, 所以调用了ViewGroup.getClipToPadding,
但这个方法是API level 21引入的. 我看了一下代码,
ViewGroup是通过调用setClipToPadding完成相关内容初始化的, setClipToPadding在API level
1就有了, 也就是说我们只要监视setClipToPadding的调用, 就能知道ViewGroup的clipToPadding状态.
如此巧妙, 如果我告诉drakeet, 说不定就能引起他的注意, 出任CEO, 走上人生巅峰.

如果你已经知道我要说什么了, 可以鄙视我.

问题

简单还原一下问题, 我们有一个类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.     } 

现在我们想随时知道mSuperX是什么值, 不用反射, 因为父类从不直接修改mSuperX的值, 总是通过setX来改, 那么最简单的方法就是继承SuperClass, 重写setX方法, 监听它的改变就好.下面是我们的子类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初始化有可能不调用setClipToPadding, 此时是默认值, 为了模拟这种情况, 将mSubX初始化为1.

最后在main里调用:


  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字节码.

下面是Main的字节码


  1. Compiled from "Main.java" 
  2. public class bugme.Main { 
  3.   ...... 
  4.   public static void main(java.lang.String[]); 
  5.     Code: 
  6.        0: new           #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明显长得不一样.

字节码乍一看怪怪的, 只要知道它隐含了一个栈和局部变量表就好懂了.

这段代码首先new一个SubClass实例, 把引用入栈, dup是把栈顶复制一份入栈, invokespecial #3将栈顶元素出栈并调用它的某个方法, 这个方法具体是什么要看常量池里第3个条目是什么, 但是javap生成的字节码直接给我们写在旁边了, 即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帮我们合成的一个方法, 里面的指令会帮我们按顺序进行普通成员变量初始化, 也包括初始化块里的代码, 注意是按顺序执行, 这些都执行完了之后才轮到构造方法里代码生成的指令执行. 这里aload_0将局部变量表中下标为0的元素入栈, 其实就是Java中的this, 结合invokespecial #1, 是在调用父类的构造函数, 也就是我们常见的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.       10: return  
  10.  
  11.   ......     

同样是先调了父类Object的构造方法, 然后再将this, 99入栈, invokevirtual #2旁边注释了是调用setX, 参数分别是this99也就是this.setX(99), 然而这个方法被重写了, 调用的是子类的方法, 所以我们再看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, 第二个是括号里的参数, 也就是99, invokespecial #3调用的是父类的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.        5: return        

这里先把this入栈, 再把参数入栈, 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                           // 即将执行这句 
  9.        6: iload_1       
  10.        7: putfield      #2                  // Field mSubX:I 
  11.       10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream; 
  12.       13: new           #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.       35: return 

从5处开始继续分析, 5,6,7将参数的值赋给mSubX, 此时mSubX是99了, 下面那一堆则是在执行System.out.println("SubX is assigned " + x);并返回, 还可以看到Java自动帮我们使用StringBuilder优化字符串拼接, 就不分析了.

说了这么多, 我们的代码才刚把下面箭头指着的这句执行完:


  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.        9: return        
  10.  
  11.   ......      

此时mSubX已经是99了, 再执行下面的4,5,6, 这一部分是SubClass的初始化, 代码将把1赋给mSubX, 99被1覆盖了.

方法返回后, 相当于我们执行完了箭头指的这一句代码:


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

接下来执行的代码将打印mSubX的值, 自然就是1了.

以前就听说过JVM是基于栈的, Dalvik是基于寄存器的, 现在看了Java字节码, 回想一下smali, 自然就能明白. 我在Android无需权限显示悬浮窗, 兼谈逆向分析app中有分析smali代码, smali里面经常看到类似v0, v1这类东西, 是在操作寄存器, 而刚才分析的bytecode, 指令常常伴随着入栈出栈.

理论解释

我们都知道Java是面向对象的语言, 面向对象三大特性之一多态性. 假如父类构造方法中调用了某个方法, 这个方法恰好被子类重写了, 会发生什么?

根据多态性, 实际被调用的是子类的方法, 这个没错. 再考虑有继承时, 初始化的顺序. 如果是new一个子类, 那么初始化顺序是:

父类static成员 -> 子类static成员 -> 父类普通成员初始化和初始化块 -> 父类构造方法 -> 子类普通成员初始化和初始化块 -> 子类构造方法

父类构造方法中调用了一次setX, 此时mSubX中已经是我们要跟踪的值, 但之后子类普通成员初始化将mSubX又初始化了一遍, 覆盖了前面我们跟踪的值, 自然得到的值就是错的.

Java中, 在构造方法中唯一能安全调用的是基类中的final方法, 自己的final方法(自己的private方法自动final), 如果类本身是final的, 自然就能安全调用自己所有的方法.

完全遵守这个准则, 可以保证不会出这个bug. 实际上我们常常不能遵守, 所以要时刻小心这个问题.

这个东西在Java编程思想(第四版) (机械工业出版社 2012年11月第1版) 的8.3.3小节有写过, 但是这种东西除非自己遇到bug了, 基本看过不会有印象.

这篇文章所有的知识点基本都是很基础的, 我自己也都记得, 但当这些知识合在一起的时候, 他们之间产生的反应却是我没有注意过的. 这也是我写这篇文章的原因.

如果以后有人面试拿这个问题考你, 你可能是遇上drakeet了.

题外话

关于默认初始化, 比如这样写:


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

如果父类保证一定会在初始化时调用setX, 程序是不会出现上面说的bug的, 因为默认初始化并不是靠生成下面这样的代码默认初始化.

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

所谓的默认初始化, 其实是我们要实例化一个对象之前, 需要一块内存放我们的数据, 这块内存被全部置为0, 这就是默认初始化了.

下面这两句话, 虽然效果一样, 但实际是有区别的.

private int mSubX;

private int mSubX = 0;

一般情况下, 这两句代码对程序没有任何影响(除非你遇到这个bug), 上面一句和下面一句的区别在于, 下面一句会导致<init>方法里面生成3条指令, 分别是aload_0, iconst_0, putfield #**, 而上面一句则不会.

所以如果你的成员变量使用默认值初始化, 就没必要自己赋那个默认值, 而且还能省3条指令.

来源:51CTO

时间: 2024-10-22 00:49:48

Java 重写方法与初始化的隐患的相关文章

Java重写方法与初始化的隐患(转)

原文出处: Shawon 虽然文章标题是Java, 但几乎所有面向对象设计的语言都遵守这个初始化流程, 感谢廖祜秋liaohuqiu_秋百万指出, 之前忘记提这个了. 前言 drakeet写了个和RecyclerView相关的GenerousRecyclerView, 原文提到了写这个的目的. 因为需要知道ViewGroup的clipToPadding属性, 所以调用了ViewGroup.getClipToPadding, 但这个方法是API level 21引入的. 我看了一下代码, View

关于java重写paint方法,求大神帮忙

问题描述 关于java重写paint方法,求大神帮忙 我能理解第一段模版函数,然后通过继承和重写方法,代入,为什么第二段代码,不需要带入父类方法,自己就跑起来了了,好像只要把paint重写了,系统自动跑, 还有中间通过屏幕监听的控制关闭的代码看不懂,为什么要这样写,老师说是匿名内部类,求大神讲解 解决方案 4444LJKHJHJHK'HJKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKJJJJJJJJJJJJJJ 解决方案二: 问题1:重新pa

在java中,有没有什么办法可以用循环的方法可以初始化很多对象到一个对象数组或者是集合当中去

问题描述 在java中,有没有什么办法可以用循环的方法可以初始化很多对象到一个对象数组或者是集合当中去 如上所述,我想要把一系列的对象初始化到一个数组或者是集合当中去,这个该怎么实现? 解决方案 List list=new ArrayList(); for(int i=0;i<10;1++){ A a=new A(); list.add(a); } 解决方案二: 不知道你想表达什么,,用一个for循环初始化后放到数组中去不就行了 解决方案三: 按照你的需求,如果你的初始化数据有规律的话,可以用循

java 接口-java重写框架中的某个方法

问题描述 java重写框架中的某个方法 有个框架的接口-当然也有框架的方法实现它的所有方法-我在开发过程中-用了实例化了该接口中的类-我现在想重新写框架中方法-必须要写个接口-这个接口继承框架的接口-写个方法来实现这个接口-问题来了-我只重写一个方法-但是现在必须重写所有的方法-得重写500个呢-怎么办-有没有好的办法-我只想重写框架中的某个方法,没有币,谢谢. 解决方案 重写方法不就方法名一样其他某些条件不一样就可以了吗 解决方案二: 1.接口继承后,实现类也继承,只是重写你要的方法 比如 框

java中方法的重写概念是相对于父类和子类的,那么同一个类到底有没有重写的概念呢

问题描述 java中方法的重写概念是相对于父类和子类的,那么同一个类到底有没有重写的概念呢 java中方法的重写概念是相对于父类和子类的,那么同一个类到底有没有重写的概念呢 解决方案 重写是在有继承关系的多个类之间,子类有与父类相同声明的方法但是方法体不同,即子类重写了父类的某些方法. 所以同一个类中显然是没有重写的概念的. 解决方案二: 没有,重写是在有继承关系的两个类中发生的. 解决方案三: 同一个类中只有重载,继承关系之间的类叫重写 解决方案四: 没有的,本类只有重载方法. 解决方案五:

java-我还想请教一下学习Java的方法及技巧

问题描述 我还想请教一下学习Java的方法及技巧 首先先对回答我的好心人表示感谢,另外我还想请教一下学习Java的方法及技巧,我的目的是掌握Java的语法,然后去学安卓开发 解决方案 看视频打代码,学算法 解决方案二: java基础的叫j2se,涉及大概封装,多态,继承,重写,重载,接口,实现等内容,io流,集合,数组,map等知识,可以参考各大学习网站!CSDN也可以哦,然后就可以继续学习安卓了!!推荐几本书籍,java编程思想,安卓推荐李刚的,适合初学者!! 解决方案三: 不用学算法,学完J

java方法-java一个方法形参有两个,如何在调用的时候只传入一个参数

问题描述 java一个方法形参有两个,如何在调用的时候只传入一个参数 如题,有一个方法里两个形参,我另一个文件类中一个方法想要那个方法的返回值,可是第二个参数在这里用不到,能否只传第一个参数 如何实现,前提这个类不能继承后重写方法,因为多人合同写的. 解决方案 不可以,变通的办法是再写一个只有一个参数的函数重载形式,在其中给另一个参数一个预设值,间接调用. 解决方案二: 一个Action调用两个不同的方法 解决方案三: 调用的时候给一个无影响的值 解决方案四: 讲道理的话是不能这样做的,不过如果

几种任务调度的 Java 实现方法与比较

综 观目前的 Web 应用,多数应用都具备任务调度的功能.本文由浅入深介绍了几种任务调度的 Java 实现方法,包括 Timer,Scheduler, Quartz 以及 JCron Tab,并对其优缺点进行比较,目的在于给需要开发任务调度的程序员提供有价值的参考.   任务调度是指基于给定时间点,给定时间间隔或者给定执行次数自动执行任务.本文由浅入深介绍四种任务调度的 Java 实现: Timer ScheduledExecutor 开源工具包 Quartz 开源工具包 JCronTab 此外

编程-java调用方法的简单问题

问题描述 java调用方法的简单问题 小弟刚刚自学java,在学习时遇到一个编程题,问题是y=x+3(x>0) =0(x=0) =x*x-1(x>0) 这是小弟编的代码, 这是报错 恳请大神帮助,!!! 解决方案 public class byt2 { public static void main(String[] args) { int x=3; int y = 0; if (x>0){ //int y=getYa(x); 已经定义过y,无需再定义了 y = getYa(x); }e