Java开发中,初始化和重写有哪些隐患?

本贴最后更新于 3294 天前,其中的信息可能已经时移世易

在 Java 开发中,我们常常都需要用到重写方法和初始化方法,但在程序实现过程中,重写和初始化的隐患大家知道多少呢?今天小编就这隐患为大家分享一二。

虽然本文是针对 Java 语言的重写和初始化讲解,但是对于所有的面向对象程序设计语言的初始化都适用,废话不多说,直接进入正题。

问题

首先我们通过代码来看一个问题,有一个类 SuperClass

public class SuperClass {

private int mSuperX;

public SuperClass() {

    setX(99);
    
}

public void setX(int x) {
 
   mSuperX = x;
    
}

}

现在我们想随时知道 mSuperX 是什么值, 不用反射, 因为父类从不直接修改 mSuperX 的值, 总是通过 setX 来改, 那么最简单的方法就是继承 SuperClass, 重写 setX 方法, 监听它的改变就可以了。下面就是子类 SubClass:

public class SubClass extends SuperClass {

private int mSubX = 1;

public SubClass() {}

@Override

public void setX(int x) {

    super.setX(x);
    
    mSubX = x;
    
    System.out.println("SubX is assigned " + x);
    
}

public void printX() {

    System.out.println("SubX = " + mSubX);
    
}

}
使用 mSubX 来跟踪 mSuperX

因为在 ViewGroup 中, clipToPadding 默认值是 true(为了简化问题, 把它当成 boolean, 实际并不是), 而 ViewGroup 初始化有可能不调用 setClipToPadding, 此时是默认值, 为了模拟这种情况, 将 mSubX 初始化为 1.

最后在 main 里调用:

public class Main {

public static void main(String[] args) {

    SubClass sc = new SubClass();
    
    sc.printX();
    
}

}

那么问题来了,终端输出的结果是什么呢?相信很多人,都 认为终端输出的结果应该是:

SubX is assigned 99

SubX = 99

其实,真正运行后输出的是:

SubX is assigned 99

SubX = 1

实际分析

  是不是很想知道,到底发生了什么?最简单的方法就是看程序到底是怎么执行的,比如单步调试, 或者直接一点,看看Java字节码。

下面是 Main 的字节码

Compiled from "Main.java"

public class bugme.Main {

......

public static void main(java.lang.String[]);

Code:

   0: new           #2                  // class bugme/SubClass
   
   3: dup           
   
   4: invokespecial #3                  // Method bugme/SubClass."<init>":()V
   
   ......  

}

这是直接用 javap 反编译.class 文件得到的。虽说同样是 Java 语言写的, 用 apktool 反编译 APK 文件(其中的 dex 文件)得到的 smali 代码和 Java Bytecode 明显长得不一样(字节码,隐含了一个栈和局部变量表)。
这段代码首先 new 一个 SubClass 实例, 把引用入栈, dup 是把栈顶复制一份入栈, invokespecial #3 将栈顶元素出栈并调用它的某个方法, 这个方法具体是什么要看常量池里第 3 个条目是什么, 但是 javap 生成的字节码直接给我们写在旁边了, 即 SubClass..

接下来看 SubClass.,

public class bugme.SubClass extends bugme.SuperClass {

public bugme.SubClass();

Code:

   0: aload_0       
   
   1: invokespecial #1                  // Method bugme/SuperClass."<init>":()V
   
   ......

这里面并没有方法叫, 是因为 javap 为了方便我们阅读, 直接把它改成类名 bugme.SubClass, 顺便说明一下, bugme 是包名。 方法并非通常意义上的构造方法, 这是 Java 帮我们合成的一个方法, 里面的指令会帮我们按顺序进行普通成员变量初始化, 也包括初始化块里的代码, 注意是按顺序执行, 这些都执行完了之后才轮到构造方法里代码生成的指令执行。 这里 aload_0 将局部变量表中下标为 0 的元素入栈, 其实就是 Java 中的 this, 结合 invokespecial #1, 是在调用父类的构造函数, 也就是我们常见的 super()。

所以我们再看 SuperClass.

public class bugme.SuperClass {

public bugme.SuperClass();

Code:

   0: aload_0       
   
   1: invokespecial #1                  // Method java/lang/Object."<init>":()V
   
   4: aload_0       
   
   5: bipush        99
   
   7: invokevirtual #2                  // Method setX:(I)V
   
  10: return 

......

}
同样是先调了父类 Object 的构造方法, 然后再将 this, 99 入栈, invokevirtual #2 旁边注释了是调用 setX, 参数分别是 this 和 99 也就是 this.setX(99), 然而这个方法被重写了, 调用的是子类的方法, 所以我们再看 SubClass.setX:

public class bugme.SubClass extends bugme.SuperClass {

......

public void setX(int);

Code:

   0: aload_0       
   
   1: iload_1       
   
   2: invokespecial #3                  // Method bugme/SuperClass.setX:(I)V
   
   ......

}

这里将局部变量表前两个元素都入栈, 第一个是 this, 第二个是括号里的参数, 也就是 99, invokespecial #3 调用的是父类的 setX, 也就是我们代码中写的 super.setX(int)

SuperClass.setX 就很简单了:

public class bugme.SuperClass {

......

public void setX(int);

Code:

   0: aload_0       
   
   1: iload_1       
   
   2: putfield      #3                  // Field mSuperX:I
   
   5: return       

}
这里先把 this 入栈, 再把参数入栈, putfield #3 使得前两个入栈的元素全部出栈, 而成员 mSuperX 被赋值, 这四条指令只对应代码里的一句 this.mSuperX = x;

接下来控制流回到子类的 setX:

public class bugme.SubClass extends bugme.SuperClass {

......

public void setX(int);

Code:

   0: aload_0       
   
   1: iload_1       
   
   2: invokespecial #3                  // Method bugme/SuperClass.setX:(I)V
   
 ->5: aload_0                           // 即将执行这句
 
   6: iload_1       
   
   7: putfield      #2                  // Field mSubX:I
   
  10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
  
  13: new           #5                  // class java/lang/StringBuilder
  
  16: dup           
  
  17: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
  
  20: ldc           #7                  // String SubX is assigned 
  
  22: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  
  25: iload_1       
  
  26: invokevirtual #9                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  
  29: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  
  32: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  
  35: return

}

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

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

public class bugme.SubClass extends bugme.SuperClass {

public bugme.SubClass();

Code:

   0: aload_0       
   
 ->1: invokespecial #1                  // Method bugme/SuperClass."<init>":()V
 
   4: aload_0       
   
   5: iconst_1      
   
   6: putfield      #2                  // Field mSubX:I
   
   9: return       

......

}

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

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

public class Main {

public static void main(String[] args) {

  ->SubClass sc = new SubClass();
  
    sc.printX();
    
}

}
接下来执行的代码将打印 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. 实际上我们常常不能遵守, 所以要时刻小心这个问题.

题外话

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

public class SubClass extends SuperClass {

private int mSubX;

public SubClass() {}

......

}

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

4: aload_0

5: iconst_1

6: putfield #2 // Field mSubX:I

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

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

private int mSubX;

private int mSubX = 0;

一般情况下, 这两句代码对程序没有任何影响(除非你遇到这个 bug), 上面一句和下面一句的区别在于, 下面一句会导致方法里面生成 3 条指令, 分别是 aload_0, iconst_0, putfield #**, 而上面一句则不会。所以如果你的成员变量使用默认值初始化, 就没必要自己赋那个默认值, 而且还能省 3 条指令。

以上就是 Java 这类面向对象语言在重写和初始化过程中,常常容易出现错误理解的地方,分享出来,希望对后续 Java 新人的学习理解有所帮助。

相关文章:《搜索量最大的 10 个 Java 问题》 http://www.maiziedu.com/group/article/6937/

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3186 引用 • 8212 回帖
  • 互联网

    互联网(Internet),又称网际网络,或音译因特网、英特网。互联网始于 1969 年美国的阿帕网,是网络与网络之间所串连成的庞大网络,这些网络以一组通用的协议相连,形成逻辑上的单一巨大国际网络。

    98 引用 • 344 回帖
  • IT
    4 引用 • 13 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • CloudFoundry

    Cloud Foundry 是 VMware 推出的业界第一个开源 PaaS 云平台,它支持多种框架、语言、运行时环境、云平台及应用服务,使开发人员能够在几秒钟内进行应用程序的部署和扩展,无需担心任何基础架构的问题。

    5 引用 • 18 回帖 • 164 关注
  • TGIF

    Thank God It's Friday! 感谢老天,总算到星期五啦!

    287 引用 • 4484 回帖 • 667 关注
  • Hexo

    Hexo 是一款快速、简洁且高效的博客框架,使用 Node.js 编写。

    21 引用 • 140 回帖 • 4 关注
  • JetBrains

    JetBrains 是一家捷克的软件开发公司,该公司位于捷克的布拉格,并在俄国的圣彼得堡及美国麻州波士顿都设有办公室,该公司最为人所熟知的产品是 Java 编程语言开发撰写时所用的集成开发环境:IntelliJ IDEA

    18 引用 • 54 回帖 • 2 关注
  • Node.js

    Node.js 是一个基于 Chrome JavaScript 运行时建立的平台, 用于方便地搭建响应速度快、易于扩展的网络应用。Node.js 使用事件驱动, 非阻塞 I/O 模型而得以轻量和高效。

    139 引用 • 269 回帖 • 48 关注
  • Shell

    Shell 脚本与 Windows/Dos 下的批处理相似,也就是用各类命令预先放入到一个文件中,方便一次性执行的一个程序文件,主要是方便管理员进行设置或者管理用的。但是它比 Windows 下的批处理更强大,比用其他编程程序编辑的程序效率更高,因为它使用了 Linux/Unix 下的命令。

    122 引用 • 73 回帖
  • 博客

    记录并分享人生的经历。

    273 引用 • 2388 回帖
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    942 引用 • 1459 回帖 • 31 关注
  • DNSPod

    DNSPod 建立于 2006 年 3 月份,是一款免费智能 DNS 产品。 DNSPod 可以为同时有电信、网通、教育网服务器的网站提供智能的解析,让电信用户访问电信的服务器,网通的用户访问网通的服务器,教育网的用户访问教育网的服务器,达到互联互通的效果。

    6 引用 • 26 回帖 • 506 关注
  • 导航

    各种网址链接、内容导航。

    39 引用 • 170 回帖 • 5 关注
  • iOS

    iOS 是由苹果公司开发的移动操作系统,最早于 2007 年 1 月 9 日的 Macworld 大会上公布这个系统,最初是设计给 iPhone 使用的,后来陆续套用到 iPod touch、iPad 以及 Apple TV 等产品上。iOS 与苹果的 Mac OS X 操作系统一样,属于类 Unix 的商业操作系统。

    84 引用 • 139 回帖 • 2 关注
  • 职场

    找到自己的位置,萌新烦恼少。

    127 引用 • 1705 回帖
  • Ant-Design

    Ant Design 是服务于企业级产品的设计体系,基于确定和自然的设计价值观上的模块化解决方案,让设计者和开发者专注于更好的用户体验。

    17 引用 • 23 回帖
  • 程序员

    程序员是从事程序开发、程序维护的专业人员。

    565 引用 • 3532 回帖
  • Log4j

    Log4j 是 Apache 开源的一款使用广泛的 Java 日志组件。

    20 引用 • 18 回帖 • 32 关注
  • HBase

    HBase 是一个分布式的、面向列的开源数据库,该技术来源于 Fay Chang 所撰写的 Google 论文 “Bigtable:一个结构化数据的分布式存储系统”。就像 Bigtable 利用了 Google 文件系统所提供的分布式数据存储一样,HBase 在 Hadoop 之上提供了类似于 Bigtable 的能力。

    17 引用 • 6 回帖 • 70 关注
  • ZooKeeper

    ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 Google 的 Chubby 一个开源的实现,是 Hadoop 和 HBase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

    59 引用 • 29 回帖 • 3 关注
  • Windows

    Microsoft Windows 是美国微软公司研发的一套操作系统,它问世于 1985 年,起初仅仅是 Microsoft-DOS 模拟环境,后续的系统版本由于微软不断的更新升级,不但易用,也慢慢的成为家家户户人们最喜爱的操作系统。

    221 引用 • 473 回帖
  • Netty

    Netty 是一个基于 NIO 的客户端-服务器编程框架,使用 Netty 可以让你快速、简单地开发出一个可维护、高性能的网络应用,例如实现了某种协议的客户、服务端应用。

    49 引用 • 33 回帖 • 19 关注
  • 钉钉

    钉钉,专为中国企业打造的免费沟通协同多端平台, 阿里巴巴出品。

    15 引用 • 67 回帖 • 337 关注
  • Ubuntu

    Ubuntu(友帮拓、优般图、乌班图)是一个以桌面应用为主的 Linux 操作系统,其名称来自非洲南部祖鲁语或豪萨语的“ubuntu”一词,意思是“人性”、“我的存在是因为大家的存在”,是非洲传统的一种价值观,类似华人社会的“仁爱”思想。Ubuntu 的目标在于为一般用户提供一个最新的、同时又相当稳定的主要由自由软件构建而成的操作系统。

    124 引用 • 169 回帖
  • 创业

    你比 99% 的人都优秀么?

    84 引用 • 1399 回帖
  • Google

    Google(Google Inc.,NASDAQ:GOOG)是一家美国上市公司(公有股份公司),于 1998 年 9 月 7 日以私有股份公司的形式创立,设计并管理一个互联网搜索引擎。Google 公司的总部称作“Googleplex”,它位于加利福尼亚山景城。Google 目前被公认为是全球规模最大的搜索引擎,它提供了简单易用的免费服务。不作恶(Don't be evil)是谷歌公司的一项非正式的公司口号。

    49 引用 • 192 回帖 • 1 关注
  • React

    React 是 Facebook 开源的一个用于构建 UI 的 JavaScript 库。

    192 引用 • 291 回帖 • 401 关注
  • 小说

    小说是以刻画人物形象为中心,通过完整的故事情节和环境描写来反映社会生活的文学体裁。

    28 引用 • 108 回帖
  • Thymeleaf

    Thymeleaf 是一款用于渲染 XML/XHTML/HTML5 内容的模板引擎。类似 Velocity、 FreeMarker 等,它也可以轻易的与 Spring 等 Web 框架进行集成作为 Web 应用的模板引擎。与其它模板引擎相比,Thymeleaf 最大的特点是能够直接在浏览器中打开并正确显示模板页面,而不需要启动整个 Web 应用。

    11 引用 • 19 回帖 • 354 关注
  • 阿里云

    阿里云是阿里巴巴集团旗下公司,是全球领先的云计算及人工智能科技公司。提供云服务器、云数据库、云安全等云计算服务,以及大数据、人工智能服务、精准定制基于场景的行业解决方案。

    89 引用 • 345 回帖