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

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

在 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 回帖
  • 互联网

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

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

相关帖子

欢迎来到这里!

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

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

推荐标签 标签

  • FreeMarker

    FreeMarker 是一款好用且功能强大的 Java 模版引擎。

    23 引用 • 20 回帖 • 464 关注
  • BND

    BND(Baidu Netdisk Downloader)是一款图形界面的百度网盘不限速下载器,支持 Windows、Linux 和 Mac,详细介绍请看这里

    107 引用 • 1281 回帖 • 36 关注
  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1063 引用 • 3455 回帖 • 153 关注
  • Openfire

    Openfire 是开源的、基于可拓展通讯和表示协议 (XMPP)、采用 Java 编程语言开发的实时协作服务器。Openfire 的效率很高,单台服务器可支持上万并发用户。

    6 引用 • 7 回帖 • 118 关注
  • 负能量

    上帝为你关上了一扇门,然后就去睡觉了....努力不一定能成功,但不努力一定很轻松 (° ー °〃)

    89 引用 • 1251 回帖 • 395 关注
  • OneDrive
    2 引用 • 3 关注
  • Thymeleaf

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

    11 引用 • 19 回帖 • 396 关注
  • 开源中国

    开源中国是目前中国最大的开源技术社区。传播开源的理念,推广开源项目,为 IT 开发者提供了一个发现、使用、并交流开源技术的平台。目前开源中国社区已收录超过两万款开源软件。

    7 引用 • 86 回帖
  • TextBundle

    TextBundle 文件格式旨在应用程序之间交换 Markdown 或 Fountain 之类的纯文本文件时,提供更无缝的用户体验。

    1 引用 • 2 回帖 • 84 关注
  • GitBook

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

    3 引用 • 8 回帖 • 4 关注
  • wolai

    我来 wolai:不仅仅是未来的云端笔记!

    2 引用 • 14 回帖 • 2 关注
  • Webswing

    Webswing 是一个能将任何 Swing 应用通过纯 HTML5 运行在浏览器中的 Web 服务器,详细介绍请看 将 Java Swing 应用变成 Web 应用

    1 引用 • 15 回帖 • 643 关注
  • HTML

    HTML5 是 HTML 下一个的主要修订版本,现在仍处于发展阶段。广义论及 HTML5 时,实际指的是包括 HTML、CSS 和 JavaScript 在内的一套技术组合。

    108 引用 • 295 回帖
  • SSL

    SSL(Secure Sockets Layer 安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS 与 SSL 在传输层对网络连接进行加密。

    70 引用 • 193 回帖 • 413 关注
  • IBM

    IBM(国际商业机器公司)或万国商业机器公司,简称 IBM(International Business Machines Corporation),总公司在纽约州阿蒙克市。1911 年托马斯·沃森创立于美国,是全球最大的信息技术和业务解决方案公司,拥有全球雇员 30 多万人,业务遍及 160 多个国家和地区。

    17 引用 • 53 回帖 • 144 关注
  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    25979 引用 • 107735 回帖
  • 小薇

    小薇是一个用 Java 写的 QQ 聊天机器人 Web 服务,可以用于社群互动。

    由于 Smart QQ 从 2019 年 1 月 1 日起停止服务,所以该项目也已经停止维护了!

    35 引用 • 468 回帖 • 761 关注
  • 又拍云

    又拍云是国内领先的 CDN 服务提供商,国家工信部认证通过的“可信云”,乌云众测平台认证的“安全云”,为移动时代的创业者提供新一代的 CDN 加速服务。

    20 引用 • 37 回帖 • 577 关注
  • Hadoop

    Hadoop 是由 Apache 基金会所开发的一个分布式系统基础架构。用户可以在不了解分布式底层细节的情况下,开发分布式程序。充分利用集群的威力进行高速运算和存储。

    93 引用 • 122 回帖 • 618 关注
  • 数据库

    据说 99% 的性能瓶颈都在数据库。

    345 引用 • 753 回帖
  • ngrok

    ngrok 是一个反向代理,通过在公共的端点和本地运行的 Web 服务器之间建立一个安全的通道。

    7 引用 • 63 回帖 • 656 关注
  • frp

    frp 是一个可用于内网穿透的高性能的反向代理应用,支持 TCP、UDP、 HTTP 和 HTTPS 协议。

    17 引用 • 7 回帖 • 2 关注
  • flomo

    flomo 是新一代 「卡片笔记」 ,专注在碎片化时代,促进你的记录,帮你积累更多知识资产。

    6 引用 • 143 回帖
  • 创业

    你比 99% 的人都优秀么?

    82 引用 • 1395 回帖 • 1 关注
  • Ant-Design

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

    17 引用 • 23 回帖 • 3 关注
  • CloudFoundry

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

    5 引用 • 18 回帖 • 189 关注
  • 深度学习

    深度学习(Deep Learning)是机器学习的分支,是一种试图使用包含复杂结构或由多重非线性变换构成的多个处理层对数据进行高层抽象的算法。

    43 引用 • 44 回帖