通过Java反编译揭开一些问题的真相

??博主在上一篇《 Java语法糖之foreach》中采用反编译的形式进行探讨进而揭开foreach语法糖的真相。进来又遇到几个问题,通过反编译之后才了解了事实的真相,觉得有必要做一下总结,也可以给各位做一下参考。

??相信很多朋友刚开始见到反编译后的内容的时候,肯定会吐槽:WTF!其实只要静下心来认真了解下,反编译也不过如此,java字节码的长度为一个字节,顶多256条指令,目前,Java虚拟机规范已经定义了其中约200条编码值对应的指令含义。这里先用一个小例子来开始我们的征程(这里只是举例,要是在真实生活中看到这种代码,估计要骂娘了):

int i=0;
int y = i++ + ++i;
i=0;
int z = i++ + ++i + ++i + ++i + i++ + ++i;

??问题来了:最后y和z分别是多少?

??看到y估计还能看看,看到z就晕乎乎的了,大家都知道i++是先取i值运算后对i进行自加,++i是先对i进行自加再运算。那么在一串组合里(y和z)怎么运用这个规则呢。

??心急的朋友估计已经打开了编译器,跑一跑答案不就出来了,看着结果再反推一下就知道这个“游戏规则”了。

??在C/C++和Java语言中都有这个事实:i++是先取i值运算后对i进行自加,++i是先对i进行自加再运算。但是这两(三)种语言跑出来的结果是不一样的。

??在c/c++中(vs6):

??运行结果:

??在java中(eclipse),运行结果:2 19。

??可以看到两(三)种语言虽然遵循了同样的自增规则但是输出的结果却不一样。这里不探讨c/c++的规则,有兴趣的同学可以追根溯源。

??那么java中遵循什么样的规则呢?这里就要祭出我们的必杀器了——反编译。

??为了防止看晕,先对这段代码进行反编译处理(先不看变量z):

package interview;
public class TestIpp
{
    public static void main(String[] args)
    {
        plus();
    }

    static void plus()
    {
        int i=0;
        int y = i++ + ++i;
        System.out.println(y);
    }
}

??对其进行反编译,反编译的命令如下:

  1. 首先切到当前文件目录下(cd命令,window和linux相同)
  2. 在当前目录下输入: javac TestIpp.java (先编译),之后会看到(window下输入dir命令,linux下输入ls命令)多出来一个TestIpp.class文件
  3. 再输入命令:javap -verbose TestIpp(反编译,注意可以没有.class),会看到反编译结果。

??上面是输入命令行的形式进行的反编译,其实Eclipse自带了这个功能,将workspace中相应的class往eclipse的workbench上一扔即可,但是javac命令生成的class文件eclipse无法识别。

??下面是反编译后的代码(篇幅限制,只显示出plus()方法的反编译内容):

  // Method descriptor #6 ()V
  // Stack: 2, Locals: 2
  static void plus();
     0  iconst_0
     1  istore_0 [i]
     2  iload_0 [i]
     3  iinc 0 1 [i]
     6  iinc 0 1 [i]
     9  iload_0 [i]
    10  iadd
    11  istore_1 [y]
    12  getstatic java.lang.System.out : java.io.PrintStream [21]
    15  iload_1 [y]
    16  invokevirtual java.io.PrintStream.println(int) : void [27]
    19  return
      Line numbers:
        [pc: 0, line: 13]
        [pc: 2, line: 14]
        [pc: 12, line: 15]
        [pc: 19, line: 16]
      Local variable table:
        [pc: 2, pc: 20] local: i index: 0 type: int
        [pc: 12, pc: 20] local: y index: 1 type: int

??这里来解析一下这些是个啥玩意儿:

     0  iconst_0        *向栈顶压入一个int常量0*,java基于栈操作,这里首先将代码[int i=0;]中的0压入栈顶
     1  istore_0 [i]    *将栈顶元素存入本地变量0[这个变量0就是i]中*,.此时栈内无元素
     2  iload_0 [i]     *将本地变量0[i]放入栈顶中*,此时栈内有一个元素,即为0
     3  iinc 0 1 [i]    *将制定的int型变量[i]增加指定值[1]*,这时i=0+1=1
     6  iinc 0 1 [i]    *将制定的int型变量[i]增加指定值[1]*,这时i=1+1=2
     9  iload_0 [i]     *将本地变量0[i]放入栈顶中*,此时栈内有两个元素,0和2,栈顶为2
    10  iadd            *将栈顶两个int类型数值相加*,结果压入栈顶,此时栈内一个元素为0+2=2
    11  istore_1 [y]    *将栈顶元素存入本地变量1中*[变量1就是y]
    12  getstatic java.lang.System.out : java.io.PrintStream [21]
    15  iload_1 [y]
    16  invokevirtual java.io.PrintStream.println(int) : void [27]
    19  return

??可以看到i++ + ++i的运行结果:遇到i++是先取i(初始i=0)的值(压入栈),然后进行自加(此时i=1),遇到+号标记继续(脑补一下逆波兰表达式,这里就不说明java的词法分析、语法分析、语义分析、代码生成的过程了),遇到++i,先进行自加(此时i=2),然后取i的值(压入栈),然后将栈顶两元素相加即可结果。

??假如有个变量m=i++ + i++ + ++i(i初始为0)那么结果等于多少呢,我们来分析一下。

??初始i=0, 遇到i++,将i的值压入栈(栈内一个元素:0),自加,此时i=1,遇到+号标记继续,遇到i++,将i值压入栈内(栈内元素:1,0),算上之前标记的+号,栈内两元素相加之后压入栈(栈内元素:1),i值自加,此时i=2,遇到+号标记继续,遇到++i,将i值自加,此时i=3压入栈内(栈内元素3,1),算上之前标记的+号,栈内两元素相加之后入栈(栈内元素为4),最后将栈顶元素存入本地变量m中,结束。整个相加过程m=0+1+3=4. 到这里,如果觉得有疑问可以打开编译器跑一下m=i++ + i++ + ++i(i初始为0)。

??那么int z = i++ + ++i + ++i + ++i + i++ + ++i(初始i=0);可以得到的结果为z=0+2+3+4+4+6=19.

??这个例子的讲解就此结束。这里博主不是想要讲解一下i++ + ++i之类的问题,而是希望大家可以通过这个问题认识学习反编译的重要性,能够更深刻的认识问题。就比如上小学一年级时,考试全是个位数加减,但是基本没人得满分,因为那时候个位数加减也是很难滴;后来到了三四年级学到乘除法的时候,个位数加减基本不会算错了;当你学到高等数学的时候你还会为普通的加减乘除烦恼嚒?会当凌绝顶,一览众山小。

??这里博主准备再将一个例子,加深一下印象,这是前几天遇到的一个问题,首先看代码举例:

package interview;

import java.util.HashMap;
import java.util.Map;

public class JavapTest2
{
    public static Map<String,String> m = new HashMap<String, String>(){
        {
            put("key1","value1");
        }
    };
}

??这段代码就是定义一个静态类成员变量m,并附初始值。很多朋友应该不太习惯这种用法,一般的就是:

public static Map<String,String> m = new HashMap<String, String>();

??要赋值就会继续m.put(“key1”,”value1”);之类的。

??那么这段代码的背后到底是什么呢?同样祭出我们的反编译。

??发现生成了两个class文件,分别为JavapTest2.class和JavapTest2$1.class.

JavapTest2.class:

// Compiled from JavapTest2.java (version 1.7 : 51.0, super bit)
public class interview.JavapTest2 {

  // Field descriptor #6 Ljava/util/Map;
  // Signature: Ljava/util/Map<Ljava/lang/String;Ljava/lang/String;>;
  public static java.util.Map m;

  // Method descriptor #10 ()V
  // Stack: 2, Locals: 0
  static {};
     0  new interview.JavapTest2$1 [12]
     3  dup
     4  invokespecial interview.JavapTest2$1() [14]    【博主自加:调用实例初始化方法】
     7  putstatic interview.JavapTest2.m : java.util.Map [17] 【博主自加:为指定的类的静态域赋值】
    10  return
      Line numbers:
        [pc: 0, line: 8]
        [pc: 10, line: 12]

  // Method descriptor #10 ()V
  // Stack: 1, Locals: 1
  public JavapTest2();
    0  aload_0 [this]
    1  invokespecial java.lang.Object() [21]
    4  return
      Line numbers:
        [pc: 0, line: 6]
      Local variable table:
        [pc: 0, pc: 5] local: this index: 0 type: interview.JavapTest2

  Inner classes:
    [inner class info: #12 interview/JavapTest2$1, outer class info: #0
     inner name: #0, accessflags: 0 default]
}

JavapTest2$1.class:

// Compiled from JavapTest2.java (version 1.7 : 51.0, super bit)
// Signature: Ljava/util/HashMap<Ljava/lang/String;Ljava/lang/String;>;
class interview.JavapTest2$1 extends java.util.HashMap {

  // Method descriptor #6 ()V
  // Stack: 3, Locals: 1
  JavapTest2$1();
     0  aload_0 [this]
     1  invokespecial java.util.HashMap() [8]          【博主自加:invokespecial是调用父类的构造函数初始化方法】
     4  aload_0 [this]
     5  ldc <String "key1"> [10]
     7  ldc <String "value1"> [12]
     9  invokevirtual interview.JavapTest2$1.put(java.lang.Object, java.lang.Object) : java.lang.Object [14] 【博主自加:调用接口方法】
    12  pop
    13  return
      Line numbers:
        [pc: 0, line: 8]
        [pc: 4, line: 10]
        [pc: 13, line: 1]
      Local variable table:
        [pc: 0, pc: 14] local: this index: 0 type: new interview.JavapTest2(){}

  Inner classes:
    [inner class info: #1 interview/JavapTest2$1, outer class info: #0
     inner name: #0, accessflags: 0 default]
  Enclosing Method: #27  #0 interview/JavapTest2
}

??可以看到生成了两个class文件,很显然这里是内部类的实现,而且是匿名内部类,不然JavapTest2$1.class的1就是其它的类名了。

??这里博主开始造“坑”了,稍微修改一下代码,如下(注意内部类中的m.put和put的区别):

package interview;

import java.util.HashMap;
import java.util.Map;

public class JavapTest2
{
    public static Map<String,String> m = new HashMap<String, String>(){
        {
            m.put("key1","value1");
        }
    };
}

??这样,发现编译器也没有报错,但是这样可不可以呢?在类中加入一个main方法:public static void main(String args[]){}运行一下,报如下错误(ExceptionInInitializerError):

Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.NullPointerException
    at interview.JavapTest2$1.<init>(JavapTest2.java:10)
    at interview.JavapTest2.<clinit>(JavapTest2.java:8)

??Why? 是不是一脸懵逼?反编译一下,你就知道。JavapTest2.class和之前的没有变化,有变化的是JavapTest2$1.class,贴出反编译结果:

// Compiled from JavapTest2.java (version 1.7 : 51.0, super bit)
// Signature: Ljava/util/HashMap<Ljava/lang/String;Ljava/lang/String;>;
class interview.JavapTest2$1 extends java.util.HashMap {

  // Method descriptor #6 ()V
  // Stack: 3, Locals: 1
  JavapTest2$1();
     0  aload_0 [this]
     1  invokespecial java.util.HashMap() [8]
     4  getstatic interview.JavapTest2.m : java.util.Map [10]
     7  ldc <String "key1"> [16]
     9  ldc <String "value1"> [18]
    11  invokeinterface java.util.Map.put(java.lang.Object, java.lang.Object) : java.lang.Object [20] [nargs: 3]
    16  pop
    17  return
      Line numbers:
        [pc: 0, line: 8]
        [pc: 4, line: 10]
        [pc: 17, line: 1]
      Local variable table:
        [pc: 0, pc: 18] local: this index: 0 type: new interview.JavapTest2(){}

  Inner classes:
    [inner class info: #1 interview/JavapTest2$1, outer class info: #0
     inner name: #0, accessflags: 0 default]
  Enclosing Method: #11  #0 interview/JavapTest2
}

??上面的第4和11(不是行号,是pc号)与修改之前的第4和9一一对应。

??这里详细解释一下这个运行流程:

??首先JavapTest2的程序入口是main方法,这个方法什么事都没干,但是这里已经触发了对JavaTest2的类的实例化(就是上面异常中的<cinit>),那么运行的是这段:

  static {};
     0  new interview.JavapTest2$1 [12]
     3  dup
     4  invokespecial interview.JavapTest2$1() [14]
     7  putstatic interview.JavapTest2.m : java.util.Map [17]
    10  return

??这段指令是首先是new JavaTest2$1这个匿名内部类,然后dup(将当前栈顶元素复制一份,并压入栈中),然后调用匿名内部类的构造函数,直到这里根本没有interview.JavapTest2.m的什么事,所以执行到这一步m并还不存在。

??接下去执行匿名内部的实例化(就是上面异常的<init>),如下:

JavapTest2$1();
     0  aload_0 [this]
     1  invokespecial java.util.HashMap() [8]
     4  getstatic interview.JavapTest2.m : java.util.Map [10]
     7  ldc <String "key1"> [16]
     9  ldc <String "value1"> [18]
    11  invokeinterface java.util.Map.put(java.lang.Object, java.lang.Object) : java.lang.Object [20] [nargs: 3]
    16  pop
    17  return

??注意到第4条getstatic interview.JavapTest2.m : java.util.Map [10]这里的getstatic是指获取指定类的静态域,但是这个m还不存在,所以是java.lang.NullPointerException,所以这段代码会报错。

附:ExceptionInInitializerError在JVM规范中这样定义:

1. 如果JVM试图创建类ExceptionInInitializerError的新实例,但是因为出现OOM而无法创建新实例,那么就抛出OOM作为代替;

2. 如果初始化器抛出一些Exception,而且Exception类不是Error或者它的某个子类,那么就会创建ExceptionInInitializerError类的一个新实例,并用Exception作为参数,用这个实例代替Exception.

时间: 2024-10-09 12:57:40

通过Java反编译揭开一些问题的真相的相关文章

Eclipse4.4 安装java反编译插件Eclipse Class Decompiler

一.在线安装方式: Eclipse Class Decompiler整合了目前最好的2个Java反编译工具Jad和JD-Core,并且和Eclipse Class Viewer无缝集成,能够很方便的使用本插件查看类库源码,以及采用本插件进行Debug调试.Eclipse Class Decompiler插件更新站点: http://feeling.sourceforge.net/update,然后直接使用Eclipse进行更新,支持Eclipse 3.x, 4.x,不依赖任何其他插件,直接勾选更

eclipse安装JAVA反编译插件

前言:在实际的开发中几乎都会使用到一些框架来辅助项目的开发工作,对于一些框架的代码我们总怀有一些好奇之心,想一探究竟,有源码当然更好了,对于有些JAR包中的代码我们就需要利用反编译工具来看一下了,下面是我常使用的一种安装JAVA反编译工具的方法,操作比较简单,不过时间长了也容易忘记,还是在此小记一笔吧!毕竟好记性不如烂笔头(插件市场中有好多好玩的,自己可以尝试玩玩看!) 1:Eclipse的版本信息 2:Help——Eclipse Marketplace 3-1:输入 Decompiler 搜索

Java 反编译工具下载

反编译,通俗来讲,就是将.java 文件经过编译生成的 .class 文件还原.注意这里的还原不等于 .java 文件.因为Java编译器在编译.java 文件的时候,会对代码进行一些处理. 那么接下来介绍几款 Java 反编译工具: 1.jad 这是一款使用很广泛的 Java 反编译工具,对 Java 底层代码的还原比较彻底.ps:个人比较喜欢这款 官网:https://varaneckas.com/jad/ 个人百度云下载地址:链接:http://pan.baidu.com/s/1jHNLx

Eclipse下的Java反编译插件 查看源代码不再困难

Eclipse下的Java反编译插件:Eclipse Class Decompiler,整合了目前最好的2个Java反编译工具Jad和JD-Core,并且和Eclipse Class Viewer无缝集成,能够很方便的使用本插件查看类库源码,以及采用本插件进行Debug调试. 转载自:http://bbs.csdn.net/topics/390263414 Eclipse Class Decompiler插件: http://download.csdn.net/detail/ibm_hoojo/

Java 反编译工具 —— JAD 的下载地址(Windows版/Linux版/Mac OS 版)

Java 反编译工具 —— JAD 的下载地址. 各种版本哦! Windows版,Linux版,Mac OS 版,等等 下载地址: http://varaneckas.com/jad/ Java 反编译工具 -- JAD 的下载地址(Windows版/Linux版/Mac OS 版),布布扣,bubuko.com

在线java反编译服务

大家是否遇到过有java class文件,却没有java源码的苦恼.近期findmaven.net提供了在线java反编译服务http://www.findmaven.net/decompile_cn.jsp.

java反编译工具(XJad)

java反编译工具(XJad) 2.2 绿色版 http://www.cr173.com/soft/35032.html Demo.class     --->    Demo.java

使用java反编译工具jad

jad支持所有内部和匿名的类 4. 命令行选择的列表 -a - 用JVM字节格式来注解输出 -af - 同 -a,但是注解的时候用全名称 -clear - 清除所有的前缀 -b - 输出多于的括号 (e.g., if(a) { b(); }, default: no) -d <dir> - 指定输出文件的文件目录 -dead -试图反编译代码的dead 部分(default: no) -disass - 不用用字节码的方式反编译 (no JAVA source generated) -f -

7 款开源 Java 反编译工具

今天我们要来分享一些关于Java的反编译工具,反编译听起来是一个非常高上大的技术词汇,通俗的说,反编译是一个对目标可执行程序进行逆向分析,从而得到原始代码的过程.尤其是像.NET.Java这样的运行在虚拟机上的编程语言,更容易进行反编译得到源代码.当然,也有一些商业软件,对其程序进行了混淆加密,这样我们就很难用工具反编译了. 1.Java反编译插件 —— Jadclipse JadClipse是Jad的Eclipse插件,是一款非常实用而且方便地Java反编译插件,我们只需将下载的插件包复制到e