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

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

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

    3169 引用 • 8208 回帖
  • 互联网

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

    96 引用 • 330 回帖
  • IT
    4 引用 • 13 回帖

相关帖子

欢迎来到这里!

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

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

推荐标签 标签

  • 域名

    域名(Domain Name),简称域名、网域,是由一串用点分隔的名字组成的 Internet 上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位(有时也指地理位置)。

    43 引用 • 208 回帖 • 3 关注
  • HBase

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

    17 引用 • 6 回帖 • 57 关注
  • ZeroNet

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

    1 引用 • 21 回帖 • 606 关注
  • Angular

    AngularAngularJS 的新版本。

    26 引用 • 66 回帖 • 530 关注
  • 链书

    链书(Chainbook)是 B3log 开源社区提供的区块链纸质书交易平台,通过 B3T 实现共享激励与价值链。可将你的闲置书籍上架到链书,我们共同构建这个全新的交易平台,让闲置书籍继续发挥它的价值。

    链书社

    链书目前已经下线,也许以后还有计划重制上线。

    14 引用 • 257 回帖
  • 小说

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

    28 引用 • 108 回帖
  • 分享

    有什么新发现就分享给大家吧!

    244 引用 • 1762 回帖
  • 导航

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

    37 引用 • 168 回帖
  • 面试

    面试造航母,上班拧螺丝。多面试,少加班。

    324 引用 • 1395 回帖 • 2 关注
  • TGIF

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

    285 引用 • 4482 回帖 • 659 关注
  • CAP

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

    11 引用 • 5 回帖 • 582 关注
  • Vditor

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

    328 引用 • 1705 回帖 • 1 关注
  • 以太坊

    以太坊(Ethereum)并不是一个机构,而是一款能够在区块链上实现智能合约、开源的底层系统。以太坊是一个平台和一种编程语言 Solidity,使开发人员能够建立和发布下一代去中心化应用。 以太坊可以用来编程、分散、担保和交易任何事物:投票、域名、金融交易所、众筹、公司管理、合同和知识产权等等。

    34 引用 • 367 回帖 • 1 关注
  • 正则表达式

    正则表达式(Regular Expression)使用单个字符串来描述、匹配一系列遵循某个句法规则的字符串。

    31 引用 • 94 回帖
  • 服务

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

    41 引用 • 24 回帖 • 11 关注
  • 开源中国

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

    7 引用 • 86 回帖
  • 架构

    我们平时所说的“架构”主要是指软件架构,这是有关软件整体结构与组件的抽象描述,用于指导软件系统各个方面的设计。另外还有“业务架构”、“网络架构”、“硬件架构”等细分领域。

    140 引用 • 441 回帖
  • 服务器

    服务器,也称伺服器,是提供计算服务的设备。由于服务器需要响应服务请求,并进行处理,因此一般来说服务器应具备承担服务并且保障服务的能力。

    124 引用 • 580 回帖
  • Sublime

    Sublime Text 是一款可以用来写代码、写文章的文本编辑器。支持代码高亮、自动完成,还支持通过插件进行扩展。

    10 引用 • 5 回帖
  • React

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

    192 引用 • 291 回帖 • 439 关注
  • JWT

    JWT(JSON Web Token)是一种用于双方之间传递信息的简洁的、安全的表述性声明规范。JWT 作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以 JSON 的形式安全的传递信息。

    20 引用 • 15 回帖 • 21 关注
  • NGINX

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

    311 引用 • 546 回帖 • 2 关注
  • 友情链接

    确认过眼神后的灵魂连接,站在链在!

    24 引用 • 373 回帖 • 3 关注
  • Oracle

    Oracle(甲骨文)公司,全称甲骨文股份有限公司(甲骨文软件系统有限公司),是全球最大的企业级软件公司,总部位于美国加利福尼亚州的红木滩。1989 年正式进入中国市场。2013 年,甲骨文已超越 IBM,成为继 Microsoft 后全球第二大软件公司。

    103 引用 • 126 回帖 • 442 关注
  • FlowUs

    FlowUs.息流 个人及团队的新一代生产力工具。

    让复杂的信息管理更轻松、自由、充满创意。

    1 引用
  • Gzip

    gzip (GNU zip)是 GNU 自由软件的文件压缩程序。我们在 Linux 中经常会用到后缀为 .gz 的文件,它们就是 Gzip 格式的。现今已经成为互联网上使用非常普遍的一种数据压缩格式,或者说一种文件格式。

    9 引用 • 12 回帖 • 124 关注
  • Linux

    Linux 是一套免费使用和自由传播的类 Unix 操作系统,是一个基于 POSIX 和 Unix 的多用户、多任务、支持多线程和多 CPU 的操作系统。它能运行主要的 Unix 工具软件、应用程序和网络协议,并支持 32 位和 64 位硬件。Linux 继承了 Unix 以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。

    920 引用 • 931 回帖