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

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

在 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 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3201 引用 • 8216 回帖 • 4 关注
  • 互联网

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

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

相关帖子

欢迎来到这里!

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

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

推荐标签 标签

  • ActiveMQ

    ActiveMQ 是 Apache 旗下的一款开源消息总线系统,它完整实现了 JMS 规范,是一个企业级的消息中间件。

    19 引用 • 13 回帖 • 677 关注
  • Hibernate

    Hibernate 是一个开放源代码的对象关系映射框架,它对 JDBC 进行了非常轻量级的对象封装,使得 Java 程序员可以随心所欲的使用对象编程思维来操纵数据库。

    39 引用 • 103 回帖 • 729 关注
  • CSDN

    CSDN (Chinese Software Developer Network) 创立于 1999 年,是中国的 IT 社区和服务平台,为中国的软件开发者和 IT 从业者提供知识传播、职业发展、软件开发等全生命周期服务,满足他们在职业发展中学习及共享知识和信息、建立职业发展社交圈、通过软件开发实现技术商业化等刚性需求。

    14 引用 • 155 回帖
  • DNSPod

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

    6 引用 • 26 回帖 • 539 关注
  • RabbitMQ

    RabbitMQ 是一个开源的 AMQP 实现,服务器端用 Erlang 语言编写,支持多种语言客户端,如:Python、Ruby、.NET、Java、C、PHP、ActionScript 等。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

    49 引用 • 60 回帖 • 349 关注
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖 • 3 关注
  • WebClipper

    Web Clipper 是一款浏览器剪藏扩展,它可以帮助你把网页内容剪藏到本地。

    3 引用 • 9 回帖 • 2 关注
  • SQLite

    SQLite 是一个进程内的库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。SQLite 是全世界使用最为广泛的数据库引擎。

    4 引用 • 7 回帖 • 4 关注
  • IDEA

    IDEA 全称 IntelliJ IDEA,是一款 Java 语言开发的集成环境,在业界被公认为最好的 Java 开发工具之一。IDEA 是 JetBrains 公司的产品,这家公司总部位于捷克共和国的首都布拉格,开发人员以严谨著称的东欧程序员为主。

    181 引用 • 400 回帖 • 1 关注
  • 服务

    提供一个服务绝不仅仅是简单的把硬件和软件累加在一起,它包括了服务的可靠性、服务的标准化、以及对服务的监控、维护、技术支持等。

    41 引用 • 24 回帖
  • etcd

    etcd 是一个分布式、高可用的 key-value 数据存储,专门用于在分布式系统中保存关键数据。

    6 引用 • 26 回帖 • 541 关注
  • NGINX

    NGINX 是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。 NGINX 是由 Igor Sysoev 为俄罗斯访问量第二的 Rambler.ru 站点开发的,第一个公开版本 0.1.0 发布于 2004 年 10 月 4 日。

    315 引用 • 547 回帖 • 1 关注
  • Log4j

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

    20 引用 • 18 回帖 • 35 关注
  • 工具

    子曰:“工欲善其事,必先利其器。”

    299 引用 • 765 回帖 • 1 关注
  • CAP

    CAP 指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可兼得。

    12 引用 • 5 回帖 • 636 关注
  • QQ

    1999 年 2 月腾讯正式推出“腾讯 QQ”,在线用户由 1999 年的 2 人(马化腾和张志东)到现在已经发展到上亿用户了,在线人数超过一亿,是目前使用最广泛的聊天软件之一。

    45 引用 • 557 回帖
  • Vditor

    Vditor 是一款浏览器端的 Markdown 编辑器,支持所见即所得、即时渲染(类似 Typora)和分屏预览模式。它使用 TypeScript 实现,支持原生 JavaScript、Vue、React 和 Angular。

    371 引用 • 1857 回帖 • 2 关注
  • MySQL

    MySQL 是一个关系型数据库管理系统,由瑞典 MySQL AB 公司开发,目前属于 Oracle 公司。MySQL 是最流行的关系型数据库管理系统之一。

    693 引用 • 537 回帖
  • 叶归
    12 引用 • 56 回帖 • 20 关注
  • Ngui

    Ngui 是一个 GUI 的排版显示引擎和跨平台的 GUI 应用程序开发框架,基于
    Node.js / OpenGL。目标是在此基础上开发 GUI 应用程序可拥有开发 WEB 应用般简单与速度同时兼顾 Native 应用程序的性能与体验。

    7 引用 • 9 回帖 • 403 关注
  • jQuery

    jQuery 是一套跨浏览器的 JavaScript 库,强化 HTML 与 JavaScript 之间的操作。由 John Resig 在 2006 年 1 月的 BarCamp NYC 上释出第一个版本。全球约有 28% 的网站使用 jQuery,是非常受欢迎的 JavaScript 库。

    63 引用 • 134 回帖 • 734 关注
  • Excel
    31 引用 • 28 回帖 • 1 关注
  • 阿里云

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

    85 引用 • 324 回帖
  • GitBook

    GitBook 使您的团队可以轻松编写和维护高质量的文档。 分享知识,提高团队的工作效率,让用户满意。

    3 引用 • 8 回帖 • 1 关注
  • MyBatis

    MyBatis 本是 Apache 软件基金会 的一个开源项目 iBatis,2010 年这个项目由 Apache 软件基金会迁移到了 google code,并且改名为 MyBatis ,2013 年 11 月再次迁移到了 GitHub。

    173 引用 • 414 回帖 • 363 关注
  • 反馈

    Communication channel for makers and users.

    120 引用 • 906 回帖 • 280 关注
  • ZeroNet

    ZeroNet 是一个基于比特币加密技术和 BT 网络技术的去中心化的、开放开源的网络和交流系统。

    1 引用 • 21 回帖 • 655 关注