第54章 Java

54.1 简介

Java程序的反编译工具已经十分成熟了。一般来讲,它们都是JVM(基于栈机制的Java虚拟机)的字节码(bytecode,指令流里的指令只有一个字节,故而得名。不过java指令中的操作数属于变长信息)分析工具。著名的JAD(http://varaneckas.com/jad/

)就是一款颇具代表性的JAVA反编译工具。

相对于x86平台更底层指令的反编译技术来说,面向JVM的bytecode更容易反编译。这主要是因为:

① 字节码含有更为丰富的数据类型信息。

② JVM内存模型更严格,因此字节码分析起来更为有章可循。

③ Java编译器不做任何优化工作(而JVM JIT在运行时会做优化工作),因此在反编译字节码之后,我们基本可以直接理解Java类文件里的原始指令。

什么时候JVM bytecode反编译有用呢?

① 无需重新编译反汇编的结果,而能给类文件做应急补丁。

② 分析混淆代码。

③ 需要编写自己的代码混淆器。

④ 创建面向JVM的、类似编译程序的代码生成工具(类似Scala,Clohure等等)。

让我们从简单的代码开始演示。除非特别指明,否则我们这里用到的都是JDK 1.7的自带工具。

反编译类文件的命令是:javap –c –verbose。

笔者采用的例子摘自于参考书目Jav13。

54.2 返回一个值

或许Java的最简函数是直接返回数值、不做其他操作的函数。当然,功能再少一点的、什么操作都没有的“闲置”函数,肯定不存在。函数必须具有某种行为,因此统称为“方法”。在Java的概念中,“类/class”是一切对象的模版,所有方法必定不能脱离“类”而单独存在。但是为了简化起见,本文还是把“方法”称为“函数”。

public class ret
{
        public static int main(String[] args)
        {
                return 0;
        }
}

我们采用命令javac 来编译它,命令行是:

javac ret.java

编译完后,我们可以用JDK自带的反汇编器-Javap来分析字节码,此时采用的命令应当是:

javap -c -verbose ret.class

反编译完成后,我们得到的代码如下所示。

指令清单54.1 JDK 1.7(摘要)

public static int main(java.lang.String[]);
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=1, locals=1, args_size=1
        0: iconst_0
        1: ireturn

Java平台的开发人员认为,0是用得最多的常量。因此他们为PUSH 0的指令单独设计了单字节的指令码,即iconst_0。此外还有iconst_1(将1入栈),iconst_2(将2入栈)……,一直到iconst_5这样的单字节字节码。而且确实有iconst_m1(将−1入栈)这类的将负数推送入栈的单字节指令。

JVM常常采用栈的方式来传递参数并从函数中返回值。因此语句iconst_0将数字0压入栈,而指令ireturn则是从栈顶返回整型数(ireturn中的字母i的意思就是“返回值为integer/整数”)。这里注意我们用TOS来代表栈顶,它是英文“Top Of Stack”的首字母缩写。

我们来重新编写一下这个例子,将返回值修改为整数1234:

public class ret
{
        public static int main(String[] args)
        {
                return 1234;
        }
}

这样的话,我们得到的结果是如下所示的代码。

指令清单54.2 JDK1.7(摘要)

public static int main(java.lang.String[]);
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=1, locals=1, args_size=1
       0: sipush           1234
       3: ireturn

指令sipush的功能是将操作数(这里是整数1234)入栈(si是short integer短型整数的缩写)。Short(短型)的意思就是针对16位的数值进行操作,而这里的整数1234正好就是一个16位的数值。

如果操作数比整型数据更大,那么字节码会是什么情况呢?让我们来看看实例:

public class ret
{
        public static int main(String[] args)
        {
                return 12345678;
        }
}

指令清单54.3 常量池

...
   #2 = Integer            12345678
...
 
  public static int main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc             #2                 // int 12345678
         2: ireturn

JVM的opcode无法直接封装32位数据,这是开发环境的局限决定的。像本例这样的32位常数将会存储到“常量池”里。常量池是一个由数组组成的表,类型为cp_info constant_pool[],用来存储程序中使用的各种常量,包括Class/String/Integer等各种基本Java数据类型,详情参见The Java Virtual Machine Specification 4.4节。

并非只有JVM如此处理常量。像MIPS、ARM以及其他的RISC型的CPU都不能在32位的opcode中封装32位常量,因此包括MIPS和ARM在内的RISC类型的CPU都得分步骤构建这些数值,或者将其保存在数据段中。有关范例可以参考本书的28.3节或者29.1节。

在MIPS的概念中也有传统意义上的常量池,不过它的名字则叫做“数据缓冲池(文字池)/literal pool”。这种文字池与可执行程序中的“.lit4/.lit8”数据段相对应。数据段.lit4用于保存32位的单精度浮点常数,而.lit8则用于保存64位的双精度浮点常数。

我们来试试其他类型的数据。

布尔型Boolean:

public class ret
{
        public static boolean main(String[] args)
        {
                return true;
        }
}
 
  public static boolean main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: iconst_1
         1: ireturn

当返回值为Ture时,JVM bytecode层面的返回值就是整数1。像C/C++一样,Java程序同样会把布尔型数值保存在32位的栈中。虽然说“逻辑真”和“整数1”的数值完全相同,但是我们不可能把布尔值当作整数值使用、也不可能把整数值当作布尔值使用。既定的类文件事先声明了数值的数据类型,而且这些数据类型会在程序运行时被实时检查。

16位的短整数型也是一样:

public class ret
{
        public static short main(String[] args)
        {
                return 1234;
        }
}
 
  public static short main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: sipush         1234
         3: ireturn

还有字符型:

public class ret
{
        public static char main(String[] args)
        {
                return 'A';
        }
}
 
  public static char main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: bipush          65
         2: ireturn

指令bipush的意思是push byte(保存字节)。Java环境中的char型数据是16位的UTF-16字符,同短整数型数据一样同属于16位short型短数据。但是大写字母A的ASCII码是十进制数65,而且我们可以用指令将一个字节的数压入栈中。

下面我们来看看byte(字节):

public class retc
{
        public static byte main(String[] args)
        {
                return 123;
        }
}
 
  public static byte main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: bipush         123
         2: ireturn

也许读者有些疑问:既然这些数据在运行的时候都是当作32位整型数据处理的,那么为什么还要不厌其烦地把它们声明为16位的数据类型呢?另外,字符型char数据与short短整数型的数值也是相同的,为什么还要刻意地把它声明为字符型char数据呢?

答案也很简单,是为了增加数据类型的控制以及增加源代码的可读性。Char字符型的限定符虽然在数值上与short短型整数相同,但是只要一看到char字符型的限定,我们立刻会联想到它是一个UTF16的字符集,而不会把它当作其他类型的方式去理解。在遇到被限定符short修饰的数据类型时,我们自然而然地就会把它理解为16位数据。同理,应当使用boolean声明的数据就不要把它声明为C语言风格的int型数据。

我们还可以通过限定符long声明JAVA的64位的整数型数据:

public class ret3
{
        public static long main(String[] args)
        {
                return 1234567890123456789L;
        }
}

指令清单54.4 常量池

...
   #2 = Long 1234567890123456789l
...
 
  public static long main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc2_w         #2         // long 1234567890123456789l
         3: lreturn

上述64位常量同样位于程序的常量池部分。它被ldc2_w指令提取之后,再由lreturn(long return)指令回传给调用方函数。ldc2_w指令也能够从常量池里提取双精度浮点数(同样是64位常量)。

public class ret
{
        public static double main(String[] args)
        {
                return 123.456d;
        }
}

指令清单54.5 常量池

...
   #2 = Double                123.456d
...
 
  public static double main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
        0: ldc2_w          #2                  // double 123.456d
        3: dreturn

这里的指令dreturn代表return double,意思是返回双精度常数。

最后,我们举一个单精度浮点数的例子。单精度浮点常数的后面有一个限定符f,而双精度数的限定符则是字母d。字母f是float的缩写,而d则是double的缩写。

public class ret
{
        public static float main(String[] args)
        {
                return 123.456f;
        }
}

指令清单54.6 常量池

...
   #2 = Float            123.456f
...
 
  public static float main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc             #2                    // float 123.456f
         2: freturn

同为从常量池提取32位数据的指令,“提取整数”和“提取浮点数”的指令都是ldc。而指令freturn代表的是return float,声明了返回值为单精度浮点数。

最后,我们来看看如果什么数也不返回时情况会是怎么样的,也就是return指令后不带任何参数。

public class ret
{
        public static void main(String[] args)
        {
                return;
        }
}
 
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return

没有返回值的函数,最后只有一条return指令。它不返回任何值,只是把程序控制流递交给调用方函数。根据函数最后一条返回值处理指令,我们就能比较容易地推导出函数返回值的数据类型。

54.3 简单的计算函数

我们继续来看看简单的计算函数:

public class calc
{
        public static int half(int a)
        {
                return a/2;
        }
}

这里我们看到的是一个除以2的简单计算函数,用到的指令是iconst_2。我们来分析一下这几条指令:

  public static int half(int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: iload_0
         1: iconst_2
         2: idiv
         3: ireturn

首先,iload_0指令是提取外来的第0个函数参数,再把它压入栈中,而iconst_2指令则是将数值2压入栈中。这两个指令执行完后,函数栈的存储内容将如下所示:

      +---+
TOS ->| 2 |
      +---+
      | a |
      +---+

TOS是“Top Of Stack”的缩写,即栈顶。

idiv指令则是从栈顶取出这两个值并进行除非运算,然后把返回的结果保存在栈顶。

      +--------+
TOS ->| result |
      +--------+

ireturn指令则会提取栈顶的数据、把它作为返回值回传给调用方函数。

下面我们来看看双精度的除法的运算指令:

public class calc
{
        public static double half_double(double a)
        {
                return a/2.0;
        }
}

指令清单54.7 常量池

...
   #2 = Double               2.0d
...
 
  public static double half_double(double);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=2, args_size=1
         0: dload_0
         1: ldc2_w         #2                // double 2.0d
         4: ddiv
         5: dreturn

双精度浮点数的运算指令和单精度浮点数的指令十分相似。在提取常量时,它使用的指令时ldc2_w。此外,所有的三条运算指令(dload_0、ddiv以及dreturn)都带有前缀d,这个限定符表明操作数属于双精度浮点数double。

下面我们来看看含有两个参数的函数情况:

public class calc
{
        public static int sum(int a, int b)
        {
                return a+b;
        }
}
 
  public static int sum(int, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: ireturn

指令iload_0用于提取第一个函数参数a,而iload_1则用于导入第二个函数参数b。在执行完这两条指令之后,栈内数据如下图所示:

      +---+
TOS ->| b |
      +---+
      | a |
      +---+

而指令iadd的含义则是将两个参数中的数值相加,并将结果保存在栈顶TOS中。

      +--------+
TOS ->| result |
      +--------+

如果我们将以上函数的两个参数的数据类型更换成long类型的话:

        public static long lsum(long a, long b)
        {
                return a+b;
        }

我们看到的字节码则会变为:

public static long lsum(long, long);
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=4, locals=4, args_size=2
       0: lload_0
       1: lload_2
       2: ladd
       3: lreturn

第二条lload指令会提取外来的第二个参数。指令后缀直接从0递增到2,是因为long型数据是64位的数据,它正好占有两个32位数据的存储位置(即后文介绍的“参数槽”)。

下面我们来看看一个更加复杂的例子:

public class calc
{
        public static int mult_add(int a, int b, int c)
        {
                return a*b+c;
        }
}

  public static int mult_add(int, int, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_0
         1: iload_1
         2: imul
         3: iload_2
         4: iadd
         5: ireturn

第一步的运算是乘法,乘积的结果存放在栈顶TOS中。

      +---------+
TOS ->| product |
      +---------+

指令iload_2将第三个参数压入栈中参加运算:

      +---------+
TOS ->|    c    |
      +---------+
      | product |
      +---------+

现在就能采用指令iadd进行加法求和运算了。

54.4 JVM的内存模型

前面提到过,在x86和其他底层运行平台上,栈通常用于传递参数的参数、存储局部变量。而我们这里要提到的JVM略有不同。

JVM的内存模型可分为:

以上3种内存模型相互独立、互相隔离。

54.5 简单的函数调用

Math.random()函数可以产生从0.0~1.0之间的任意(伪)随机数。因此,如欲生成0.0~0.5之间的随机数,就要对上述结果进行除法运算:

public class HalfRandom
{
        public static double f()
        {
                return Math.random()/2;
        }
}

指令清单54.8 常量池

...
   #2 = Methodref         #18.#19         // java/lang/Math.random:()D
   #3 = Double            2.0d
...
  #12 = Utf8              ()D
...
  #18 = Class             #22             // java/lang/Math
  #19 = NameAndType       #23:#12         // random:()D
  #22 = Utf8              java/lang/Math
  #23 = Utf8              random
 
  public static double f();
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=0, args_size=0
         0: invokestatic  #2              // Method java/lang/Math.random:()D
         3: ldc2_w        #3              // double 2.0d
         6: ddiv
         7: dreturn

指令invokestatic调用函数Math.random(),并将结果保存在栈顶TOS。这个值随后被除以2,最终成为函数返回值。但是这些函数的名称是如何编码的?编译器使用了Methodref的表达方法把外部函数的信息编排在常量池中。常量池里的相应数据声明了与被调用函数有关的类(Class)以及方法(NameAndType)名称。Methodref表达式的第一个字段(Fieldref里的第一个值)是Class的索引号-#18,这个索引号(实际上是指针)对应着一个Class名称(依次查询#18、#22号常量,可得到java/lang/Math)。Methodref表达式的第二个字段(Fieldref里的第二个值)是方法名称的索引号#19,这个索引号对应着方法名称NameAndType(依次查询#19、#23号常量,可得到方法名称random)。方法名称由2个索引号组成,第一个索引号对应着外部函数名称,而第二个索引号对应着函数返回值的数据类型“()D”——双精度浮点数。

综合上述信息可知:

① JVM能检查数据类型的正确性。

② JAVA的反编译器能从已经编译好的类文件中恢复出其原来的数据类型。

最后,我们来看一个经典的字符串显示例子:Hello, world!

public class HelloWorld
{
        public static void main(String[] args)
        {
                System.out.println("Hello, World");
        }
}

指令清单54.9 常量池

...
   #2 = Fieldref           #16.#17         // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18             // Hello, World
   #4 = Methodref          #19.#20         // java/io/PrintStream.println:(Ljava/lang/String;)V
...
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               Hello, World
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
...
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
...
 
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic      #2            // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc            #3            // String Hello, World
         5: invokevirtual  #4            // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return

偏移量为3的ldc指令从常量池中提取字符串Hello,World的指针,然后将其压入栈中。在Java中,这种二级指针称为reference(引用),但是它的本质仍然还是指针或者地址。[1]

熟悉的指令invokevirtual从常量池中提取println函数的信息,然后调用该函数。我们已经知道,标准库定义了许多版本的println()函数,每个版本都处理的数据类型都各不相同。本例调用的println()函数,肯定是专门处理string型数据的那个版本。

第一条指令getstatic的功能是什么呢?这个指令从对象System.out中提取引用指针的有关字段,再把它压入栈。这个引用指针的作用与println方法的this指针相似。因此,从内部来讲,println函数的输入参数实际上是两个指针:①this指针,也就是指向对象的指针;②字符串“Hello,World”的地址。

因此这并不矛盾:只有在System.out初始化为实例的时候,才能调用println()方法。

为了方便分析人员阅读, javap把有关信息全部追加到了字节码的注释里了。

54.6 调用函数beep()(蜂鸣器)

这是一个最简单的调用(调用了无参数的两个函数),其功能就是发出蜂鸣声beep:

    public static void main(String[] args)
    {
            java.awt.Toolkit.getDefaultToolkit().beep();
    };
 
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: invokestatic   #2                  // Method java/awt/Toolkit.getDefaultToolkit:()↙
    ↘ Ljava/awt/Toolkit;
         3: invokevirtual  #3                  // Method java/awt/Toolkit.beep:()V
         6: return

第一条指令是偏移量为0的invokestatic指令,它调用了java.awt.Toolkit.getDefaultToolkit()函数。后者的返回值是Toolkit Class类实例的引用指针。偏移量为3的invokevirtual指令调用这个类的beep()函数。

54.7 线性同余随机数产生器(PRNG)

我们再来看看PRNG(Pseudo Random Numbers Generator)随机数产生器,其实我们已经在本书的第20章介绍过它的C语言代码了。

public class LCG
{
        public static int rand_state;

        public void my_srand (int init)
        {
                rand_state=init;
        }

        public static int RNG_a=1664525;
        public static int RNG_c=1013904223;

        public int my_rand ()
        {
                rand_state=rand_state*RNG_a;
                rand_state=rand_state+RNG_c;
                return rand_state & 0x7fff;
        }
}

程序在启动之初就初始化了数个成员变量(Class Fields)。这是如何进行的呢?这就得借助javap查看该类的构造函数:

  static {};
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc            #5                  // int 1664525
         2: putstatic      #3                  // Field RNG_a:I
         5: ldc            #6                  // int 1013904223
         7: putstatic      #4                  // Field RNG_c:I
        10: return

上述指令展示了变量的初始化过程。变量RNG_a和RNG_c分别占据参数槽的第三和第四存储单元。而putstatic函数将有关常数存放在相应地址。

函数my_rand()将输入值保存在变量rand_state中:

  public void my_srand(int);
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: iload_1
         1: putstatic      #2                  // Field rand_state:I
         4: return

Iload_1指令提取输入变量,然后将其压入栈中。但是为什么此处是iload_1指令而不是读取第0个参数的iload_0指令?这是由于该函数调用了类的成员变量,因此要用第0个参数传递this指针。依此类推,函数的第二个参数槽用于传递成员变量、同时是隐性参数rand_state,此后的putstatic指令把栈顶的数值肤之道第二个参数槽、完成指定任务。

现在我们来看看my_rand()函数:

  public int my_rand();
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic      #2                  // Field rand_state:I
         3: getstatic      #3                  // Field RNG_a:I
         6: imul
         7: putstatic      #2                  // Field rand_state:I
        10: getstatic      #2                  // Field rand_state:I
        13: getstatic      #4                  // Field RNG_c:I
        16: iadd
        17: putstatic      #2                  // Field rand_state:I
        20: getstatic      #2                  // Field rand_state:I
        23: sipush         32767
        26: iand
        27: ireturn

这段代码分别提取类实例的各成员变量、进行各种运算,再使用putstatic指令更新rand_state的值。在偏移量为20处,rand_state的值会重新调入(此前的putstatic指令把它从栈里抛了出去)。虽然表面看来这个程序的效率很低,但是JVM肯定能够进行充分的优化、足以弥补字节码的效率缺陷。

54.8 条件转移

我们来看一个简单的例子:

public class abs
{
        public static int abs(int a)
        {
                if (a<0)
                        return -a;
                return a;
        }
}
 
  public static int abs(int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: iload_0
         1: ifge            7
         4: iload_0
         5: ineg
         6: ireturn
         7: iload_0
         8: ireturn

如果栈顶/TOS的值大于或等于零,那么ifge将会跳转到偏移量为7的指令。特别需要注意的是,ifxx指令还会从栈顶抛弃一个值,否则它就不能进行比较运算。

后面的ineg指令是对整数求负的运算指令。

我们再看一个例子:

        public static int min (int a, int b)
        {
                if (a>b)
                        return b;
                return a;
        }

这个函数的字节码如下所示:

  public static int min(int, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: iload_0
         1: iload_1
         2: if_icmple       7
         5: iload_1
         6: ireturn
         7: iload_0
         8: ireturn

if_icmple指令从栈中提取(pop)两个数值并将之进行比较。如果第二个操作数小于或等于第一个操作数,那么它将跳转到偏移量为7的指令,否则继续执行下一条指令。

若对上述程序进行调整,通过较大值函数max()进行比较:

        public static int max (int a, int b)
        {
                if (a>b)
                        return a;
                return b;
        }

那么字节码则会变为:

  public static int max(int, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: iload_0
         1: iload_1
         2: if_icmple      7
         5: iload_0
         6: ireturn
         7: iload_1
         8: ireturn

其实与刚才取较小值的程序基本相同,但是最后两个iload指令(在偏移量为5和7的位置上)位置对换了一下。

下面再看一个更复杂一些的例子:

public class cond
{
        public static void f(int i)
        {
                if (i<100)
                        System.out.print("<100");
                if (i==100)
                        System.out.print("==100");
                if (i>100)
                        System.out.print(">100");
                if (i==0)
                        System.out.print("==0");
        }
}
 
  public static void f(int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: iload_0
         1: bipush         100
         3: if_icmpge      14
         6: getstatic      #2          // Field java/lang/System.out:Ljava/io/PrintStream;
         9: ldc            #3          // String <100
        11: invokevirtual  #4          // Method java/io/PrintStream.print:(Ljava/lang/String;)V
        14: iload_0
        15: bipush         100
        17: if_icmpne      28
        20: getstatic      #2          // Field java/lang/System.out:Ljava/io/PrintStream;
        23: ldc            #5          // String ==100
        25: invokevirtual  #4          // Method java/io/PrintStream.print:(Ljava/lang/String;)V
        28: iload_0
        29: bipush         100
        31: if_icmple      42
        34: getstatic      #2          // Field java/lang/System.out:Ljava/io/PrintStream;
        37: ldc            #6          // String >100
        39: invokevirtual  #4          // Method java/io/PrintStream.print:(Ljava/lang/String;)V
        42: iload_0
        43: ifne           54
        46: getstatic      #2          // Field java/lang/System.out:Ljava/io/PrintStream;
        49: ldc #7 // String ==0
        51: invokevirtual  #4         // Method java/io/PrintStream.print:(Ljava/lang/String;)V
        54: return

这段代码用以实现两个功能。首先,它可以判断输入的数与100的大小关系,如果是小于100的话,显示“<100”的字样;如果是等于100,则显示“=100”的字样;如果是大于100的话,则显示“>100”的字样。另外一个功能是一个特例,就是如果输入的数为0的话,则显示“==0”字样。

我们这里还是用到了前面提到的指令ifXX。还记得其功能吧?

指令if_icmpge从栈中提取(pop)出两个值,然后对它们进行比较。如果第二个数大于第一个数的话,那么就跳转到偏移量为14的位置;其实指令if_icmpne和指令if_icmple的运行机理基本相同,只是其转移的条件不相同而已。

在偏移量为43的位置,我们还可以看到一个指令ifne。我们认为这是一个措辞不当的助记符,如果把它的助记符换位ifnz似乎更加贴切一些(意思是当栈顶的值不是0时跳转),而实际的执行过程也是这样的——当输入的值不是0时,程序会跳转到偏移量为54的地方。而如果输入值为0,程序的执行流则不会发生跳转、继续执行偏移量为46的指令、显示“==0”这个字符串。

必须注意的是:JVM没有无符号数的数据类型。因此我们只会遇到比较有符号数的比较指令。

54.9 传递参数

我们来将前面讲到的两个取较大值和较小值的函数混合在一起使用,也就是函数min()和函数max()。

public class minmax
{
        public static int min (int a, int b)
        {
                if (a>b)
                        return b;
                return a;
        }

        public static int max (int a, int b)
        {
                if (a>b)
                        return a;
                return b;
        }

        public static void main(String[] args)
        {
                int a=123, b=456;
                int max_value=max(a, b);
                int min_value=min(a, b);
                System.out.println(min_value);
                System.out.println(max_value);
        }
}

下面是主函数main()的代码:

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=5, args_size=1
         0: bipush        123
         2: istore_1
         3: sipush        456
         6: istore_2
         7: iload_1
         8: iload_2
         9: invokestatic  #2             // Method max:(II)I
        12: istore_3
        13: iload_1
        14: iload_2
        15: invokestatic  #3             // Method min:(II)I
        18: istore 4
        20: getstatic     #4             // Field java/lang/System.out:Ljava/io/PrintStream;
        23: iload 4
        25: invokevirtual #5             // Method java/io/PrintStream.println:(I)V
        28: getstatic     #4             // Field java/lang/System.out:Ljava/io/PrintStream;
        31: iload_3
        32: invokevirtual #5             // Method java/io/PrintStream.println:(I)V
        35: return

调用方函数通过栈向被调用方函数传递参数,而被调用方函数通过TOS()/栈顶向调用方函数传递返回值。

54.10 位操作

JVM的位操作指令和其他指令集的工作原理基本相同。

        public static int set (int a, int b)
        {
                return a | 1<<b;
        }

        public static int clear (int a, int b)
        {
                return a & (~(1<<b));
        }
 
  public static int set(int, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=2
         0: iload_0
         1: iconst_1
         2: iload_1
         3: ishl
         4: ior
         5: ireturn

  public static int clear(int, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=2
         0: iload_0
         1: iconst_1
         2: iload_1
         3: ishl
         4: iconst_m1
         5: ixor
         6: iand
         7: ireturn

指令iconst_m1将−1这个数调入栈中,其实这个值就是0Xffffffff。将任意数与−1进行异或XOR运算,其实就是对原操作数的所有位逐位取反(这一点可以参见本书的附录A.6.2)。

而当我们将所有的数据类型扩展为64位的long类型时:

        public static long lset (long a, int b)
        {
                return a | 1<<b;
        }

        public static long lclear (long a, int b)
        {
                return a & (~(1<<b));
        }
 
  public static long lset(long, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=3, args_size=2
         0: lload_0
         1: iconst_1
         2: iload_2
         3: ishl
         4: i2l
         5: lor
         6: lreturn

  public static long lclear(long, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=3, args_size=2
         0: lload_0
         1: iconst_1
         2: iload_2
         3: ishl
         4: iconst_m1
         5: ixor
         6: i2l
         7: land
         8: lreturn

除了操作指令都具有一个表示操作数是64位值的“L”前缀之外,这个程序的字节码和上一个程序几乎相同。此外,第二个函数的参数还有一个整型数据。假如需要把int型的32位数据扩展为64位long型数据,那么编译器就会分配i2l完成这项任务。

54.11 循环

public class Loop
{
        public static void main(String[] args)
        {
                for (int i = 1; i <= 10; i++)
                {
                        System.out.println(i);
                }
        }
}
 
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_1
         1: istore_1
         2: iload_1
         3: bipush         10
         5: if_icmpgt      21
         8: getstatic      #2           // Field java/lang/System.out:Ljava/io/PrintStream;
        11: iload_1
        12: invokevirtual  #3           // Method java/io/PrintStream.println:(I)V
        15: iinc           1, 1
        18: goto           2
        21: return

这里举一个例子,显示从1~10一共10个整型数。采用循环指令的方式。

指令iconst_1将数值1调入栈顶TOS,而istore_1指令则将局部变量阵列LVA中的这项数值存储在第一个参数槽里。为什么把它存储在1号参数槽而不是第0个参数槽呢?这是因为主函数main()有一个参数是String数组,这个字符串的引用指针会占用第0号参数槽。

因此,局部变量i必须存放于第1个参数槽。

在偏移量为3和5的地方的指令,分别将变量i与循环控制变量的上限(这里是10)比较。如果此时的变量i比10大,那么指令流就会转向偏移量为21的地方,直接退出函数;否则,程序就会调用println()函数显示当前的数值。显示完后,在偏移量为11的地方,局部变量i会重新装入新的值,继续为显示方法做准备。另外,在调用println方法的时候,我们提供的参数时integer型参数。注释中的“(I)V”分别表示数据类型为integer,函数类型为void。

当显示函数println结束时,i的数值在偏移量为15的地方递增,也就是加1。这条指令有两个操作数。第一个操作数,第一个操作数表示实际运算数存储于第一号参数槽,第二个操作数表示递增的增量是1。

goto指令的功能就是跳转/GOTO。它跳转到循环体中偏移量为2的地方。

让我们来看一个稍微复杂一些的例子:斐波那契数列,简称为Fibonacci,其实前面已经提到过了。但是这里我们来看看如何用程序来实现它。

public class Fibonacci
{
        public static void main(String[] args)
        {
                int limit = 20, f = 0, g = 1;

                for (int i = 1; i <= limit; i++)
                {
                        f = f + g;
                        g = f - g;
                        System.out.println(f);
                }
        }
}
 
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=5, args_size=1
         0: bipush         20
         2: istore_1
         3: iconst_0
         4: istore_2
         5: iconst_1
         6: istore_3
         7: iconst_1
         8: istore         4
        10: iload          4
        12: iload_1
        13: if_icmpgt      37
        16: iload_2
        17: iload_3
        18: iadd
        19: istore_2
        20: iload_2
        21: iload_3
        22: isub
        23: istore_3
        24: getstatic      #2           // Field java/lang/System.out:Ljava/io/PrintStream;
        27: iload_2
        28: invokevirtual  #3           // Method java/io/PrintStream.println:(I)V
        31: iinc           4, 1
        34: goto           10
        37: return

我们来看看本地存储数组(Local Varible Array,LVA)与各参数槽的存储关系:

① 0号参数槽存储的是主函数main()的唯一参数;

② 1号参数槽存储的是循环控制变量limit,其值固定为20;

③ 2号参数槽存储的是变量f

④ 3号参数槽存储的是变量g

⑤ 4号参数槽存储的是变量i

可见,Java编译器会按照源代码声明变量的顺序,在LVA中依次分配各变量的存储空间。

当直接向第0、1、2、3号参数槽存储数据时,可使用专用的istore_n指令。然而当直接向4及更高编号的参数槽存储数据时,就没有这样便利的专用操作指令了,需要使用带有参数的istore指令。如偏移量为8的指令所示,后一种istore指令将操作数当作参数槽的编号进行存储操作。其实iload指令也是如此。本文就不再解释偏移量为10的iload指令了。

但是,像循环迭代上限limit这样的常量也占用了参数槽,难道它还经常更新数值吗?JVM JIT编译器能够充分优化这类事务,我们不必专们进行人工干预。

54.12 switch()语句

下列范例证明,switch()语句是由tableswitch指令实现的。

        public static void f(int a)
        {
                switch (a)
                {
                case 0: System.out.println("zero"); break;
                case 1: System.out.println("one\n"); break;
                case 2: System.out.println("two\n"); break;
                case 3: System.out.println("three\n"); break;
                case 4: System.out.println("four\n"); break;
                default: System.out.println("something unknown\n"); break;
                };
        }

上述程序的字节码与源程序几乎是逐一对应:

  public static void f(int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: iload_0
         1: tableswitch    { // 0 to 4
                       0: 36
                       1: 47
                       2: 58
                       3: 69
                       4: 80
                 default: 91
            }
        36: getstatic     #2         // Field java/lang/System.out:Ljava/io/PrintStream;
        39: ldc           #3         // String zero
        41: invokevirtual #4         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        44: goto          99
        47: getstatic     #2         // Field java/lang/System.out:Ljava/io/PrintStream;
        50: ldc           #5         // String one\n
        52: invokevirtual #4        // Method java/io/PrintStream.println:(Ljava/lang /String;)V
        55: goto          99
        58: getstatic     #2         // Field java/lang/System.out:Ljava/io/PrintStream;
        61: ldc           #6         // String two\n
        63: invokevirtual #4         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        66: goto          99
        69: getstatic     #2         // Field java/lang/System.out:Ljava/io/PrintStream;
        72: ldc           #7         // String three\n
        74: invokevirtual #4         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        77: goto          99
        80: getstatic     #2         // Field java/lang/System.out:Ljava/io/PrintStream;
        83: ldc           #8         // String four\n
        85: invokevirtual #4         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        88: goto          99
        91: getstatic     #2         // Field java/lang/System.out:Ljava/io/PrintStream;
        94: ldc           #9         // String something unknown\n
        96: invokevirtual #4         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        99: return

如果输入值是0,则显示为zero;如果输入值是1,则显示one;如果输入值是2,则显示two;如果输入值是3,则显示three;如果输入值是4,则显示four;如果不是以上的5种情况,则显示字符串something unknown。

54.13 数组

54.13.1 简单的例子

我们首先创建一个含有10个元素的整数型数组,然后逐次填入0~9:

程序如下:

        public static void main(String[] args)
        {
                int a[]=new int[10];
                for (int i=0; i<10; i++)
                        a[i]=i;
                dump (a);
        }
 
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: bipush       10
         2: newarray     int
         4: astore_1
         5: iconst_0
         6: istore_2
         7: iload_2
         8: bipush       10
        10: if_icmpge    23
        13: aload_1
        14: iload_2
        15: iload_2
        16: iastore
        17: iinc          2, 1
        20: goto          7
        23: aload_1
        24: invokestatic  #4               // Method dump:([I]V
        27: return

指令newarray创建一个可容纳10个整型(int)元素的数组。这个数组的大小是由bipush设定的,它会被保存在栈顶TOS;而数组的类型则是由newarray指令的操作数定义。我们看到newarrary的操作数是int,因此它会创建整型数组。执行完指令newarray后,系统会给新建的数组分配一个引用指针(reference),并且把这个引用指针存储到栈顶TOS。其后的astore_1指令吧引用指针存储到LVA的第一个参数槽。主函数main()的第二部分是一个循环语句。这个循环执行的指令将变量 i 依次保存到相应的数值单元中。指令aload_1获取数组的引用指针,并且把它保存在栈中。而指令iastore的功能则把栈里的整型数据保存在数组中,与此同时它会通过栈顶TOS获取数组的引用指针。而主函数main()的第三部分的功能是执行函数dump()。在偏移量为23的地方,我们可以看到aload_1指令。它负责制备dump()的唯一参数。

下面我们来继续看看函数dump()的功能:

        public static void dump(int a[])
        {
                for (int i=0; i<a.length; i++)
                        System.out.println(a[i]);
        }

该函数执行的功能是循环显示目标数组中的值。

程序如下所示。

  public static void dump(int[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: aload_0
         4: arraylength
         5: if_icmpge      23
         8: getstatic      #2           // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_0
        12: iload_1
        13: iaload
        14: invokevirtual  #3           // Method java/io/PrintStream.println:(I)V
        17: iinc           1, 1
        20: goto           2
        23: return

函数会从第0号参数槽获取数组的引用指针/reference。而源代码中的a.length表达式被编译器转换成了arraylength(数组长度)指令:它通过引用指针获取数组的信息,并把数组长度保存在栈顶TOS中。在偏移量为13的指令iaload则负责加载既定的数组元素。在数组类型确定的情况下,对某个数组元素寻址需要知道数组的首地址和即定元素的索引编号。前者由偏移量为11的指令aload_0完成;后者则通过偏移量为12的指令iload_1实现。

很多人会想当然的认为,在那些带有字母前缀a的指令里,a大概是数组array的缩写。其实这种猜测并不确切。此类指令是操作数据对象引用指针的指令。数组和字符串只是对象型数据的一种特例罢了。

54.13.2 数组元素求和

我们来看看另外一个例子,其功能是将一个输入的数组的各项数值相加求和。

public class ArraySum
{
        public static int f (int[] a)
        {
                int sum=0;
                for (int i=0; i<a.length; i++)
                        sum=sum+a[i];
                return sum;
        }
}
 
  public static int f(int[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: iconst_0
         3: istore_2
         4: iload_2
         5: aload_0
         6: arraylength
         7: if_icmpge      22
        10: iload_1
        11: aload_0
        12: iload_2
        13: iaload
        14: iadd
        15: istore_1
        16: iinc           2, 1
        19: goto           4
        22: iload_1
        23: ireturn

在这个成员函数的存储空间里,外来数组的引用指针存储于LVA的0号存储槽里,而局部变量sum则存储在LVA的1号存储槽里。

54.13.3 输入变量为数组的主函数main()

下面展示的是一个单参数的main()函数。这个外来参数是一个字符串。

public class UseArgument
{
        public static void main(String[] args)
        {
                System.out.print("Hi, ");
                System.out.print(args[1]);
                System.out.println(". How are you?");
        }
}

第0个参数是程序名(就像在C/C++等中的一样),因此程序员指定的第一个参数存放于第一个参数槽。

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic      #2        // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc            #3        // String Hi,
         5: invokevirtual  #4        // Method java/io/PrintStream.print:(Ljava/lang/String;)V
         8: getstatic      #2        // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_0
        12: iconst_1
        13: aaload
        14: invokevirtual  #4        // Method java/io/PrintStream.print:(Ljava/lang/String;)V
        17: getstatic      #2        // Field java/lang/System.out:Ljava/io/PrintStream;
        20: ldc            #5        // String . How are you?
        22: invokevirtual  #6        // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        25: return

偏移量为11的指令aload_0加载了LVA(局部变量数组)的第0个存储单元。而函数main()的唯一一个指定参数则通过偏移量为12和13的iconst_1和aaload指令的作用是获取数组的第一个元素的引用指针(也就是索引号为0的元素首地址)。偏移量为14的指令通过TOS向被调用方函数传递字符串的引用指针,这个引用指针就是此后println方法的输入变量。

54.13.4 预设初始值的数组
class Month
{
        public static String[] months =
        {
                "January",
                "February",
                "March",
                "April",
                "May",
                "June",
                "July",
                "August",
                "September",
                "October",
                "November",
                "December"
        };

        public String get_month (int i)
        {
                return months[i];
        };
}

我们在这里列举一个有预设值的数组,它的元素是字符串型的,其值是从一月到十二月的英文单词,分别是January、February、March、April、May、June、July、August、September、October、November和December。

函数get_month的功能比较简单,它是输入一个整型的数,从而能在数组的对应位置输出相应月份的字符串。

  public java.lang.String get_month(int);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: getstatic      #2            // Field months:[Ljava/lang/String;
         3: iload_1
         4: aaload
         5: areturn

aaload指令从栈里POP出数组的引用指针和元素的索引编号,并将指定元素推送入栈。在Java的概念里,字符串是对象型数据。在操作对象型数据(确切的说是引用指针)时应当使用带有a前缀的指令。同理,后面的areturn指令从侧面印证了返回指是字符串对象的引用指针。

另外一个问题是:数组months[]是如何被初始化的呢?也就是说,这个数组的初始值1到12月份的英文字符串是如何被相应地赋值到数组的相关位置的?

  static {};
    flags: ACC_STATIC
    Code:
      stack=4, locals=0, args_size=0
         0: bipush         12
         2: anewarray      #3            // class java/lang/String
         5: dup
         6: iconst_0
         7: ldc            #4            // String January
         9: aastore
        10: dup
        11: iconst_1
        12: ldc            #5            // String February
        14: aastore
        15: dup
        16: iconst_2
        17: ldc            #6            // String March
        19: aastore
        20: dup
        21: iconst_3
        22: ldc            #7            // String April
        24: aastore
        25: dup
        26: iconst_4
        27: ldc            #8            // String May
        29: aastore
        30: dup
        31: iconst_5
        32: ldc            #9            // String June
        34: aastore
        35: dup
        36: bipush         6
        38: ldc            #10           // String July
        40: aastore
        41: dup
        42: bipush         7
        44: ldc            #11           // String August
        46: aastore
        47: dup
        48: bipush         8
        50: ldc            #12           // String September
        52: aastore
        53: dup
        54: bipush         9
        56: ldc            #13           // String October
        58: aastore
        59: dup
        60: bipush         10
        62: ldc            #14           // String November
        64: aastore
        65: dup
        66: bipush         11
        68: ldc            #15           // String December
        70: aastore
        71: putstatic      #2            // Field months:[Ljava/lang/String;
        74: return

指令anewarray负责创建一个指定大小的数组,并将对象的引用指针推送入栈(字母a表示返回值为引用指针)。anewarry指令的操作数声明了目标数组的数据类型,在上面的指令里这个操作数是java/lang/String。在此之前的bipush 12则设置了数组的大小(这个值会被anewarrary指令pop出栈),而这个大小正好是一年的月份总数。后面出现的dup指令是在栈计算机领域非常著名的栈顶复制指令(在Forth等基于堆栈的编程语言里都有这条指令)。它将复制数组的引用指针。这是因为aastore指令会从栈顶pop出引用指针,而后面的aastore指令还需要再次读取该引用指针。显而易见的是,Java编译器认为在存储数组元素时分配dup指令比分配getstatic指令更为稳妥,否则它也不会一口气派发了12个dup指令。

aastore指令从TOS里依次提取(即POP)元素值、数组下标和数组的引用指针,并将指定值存储到指定的数组元素里。

最后的putstatic指令将栈顶的数据出栈并把它存储到常量解析池的#2号位置。因此它把新建数组的引用指针保存到了整个实例的第二个字段,也就是给months字段赋值。

54.13.5 可变参数函数

可变参数函数利用了数组的数据结构。

        public static void f(int... values)
        {
                for (int i=0; i<values.length; i++)
                        System.out.println(values[i]);
        }

        public static void main(String[] args)
        {
                f (1,2,3,4,5);
        }
  public static void f(int...);
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=3, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: aload_0
         4: arraylength
         5: if_icmpge      23
         8: getstatic      #2            // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_0
        12: iload_1
        13: iaload
        14: invokevirtual  #3            // Method java/io/PrintStream.println:(I)V
        17: iinc           1, 1
        20: goto           2
        23: return

在f()函数中,偏移量为3的aload_0指令提取了整数数组的指针。此后的指令依次提取数组大小等信息。

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=1, args_size=1
         0: iconst_5
         1: newarray       int
         3: dup
         4: iconst_0
         5: iconst_1
         6: iastore
         7: dup
         8: iconst_1
         9: iconst_2
        10: iastore
        11: dup
        12: iconst_2
        13: iconst_3
        14: iastore
        15: dup
        16: iconst_3
        17: iconst_4
        18: iastore
        19: dup
        20: iconst_4
        21: iconst_5
        22: iastore
        23: invokestatic   #4            // Method f:([I]V
        26: return

main()函数通过newarray指令构造了一个数组,接着填充这个数组,随后调用了f()函数。

虽然newarray属于某种构造函数,但是在main()结束之后整个数组没有被析构函数释放。实际上Java没有析构函数。JVM具有自动的垃圾回收机制。

另外,当函数main()退出后,数组对象的值其实是未消失的。其实在JAVA环境中就没有清除这个指令,原因是JAVA的内存机制会自动清理不用内存的功能,当然是在其认为必要时进行。

系统自带的format()方法又是如何处理可变参数的呢?它把输入参数分为了字符串对象和数组型对象两大部分:

        public PrintStream format(String format, Object... args)

参考链接:http://docs.oracle.com/javase/tutorial/java/data/numberformat.html

我们再来看看下面的例子:

        public static void main(String[] args)
        {
                int i=123;
                double d=123.456;
                System.out.format("int: %d double: %f.%n", i, d);
        }
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=7, locals=4, args_size=1
         0: bipush         123
         2: istore_1
         3: ldc2_w         #2         // double 123.456d
         6: dstore_2
         7: getstatic      #4         // Field java/lang/System.out:Ljava/io/PrintStream;
        10: ldc            #5         // String int: %d double: %f.%n
        12: iconst_2
        13: anewarray      #6         // class java/lang/Object
        16: dup
        17: iconst_0
        18: iload_1
        19: invokestatic   #7         // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        22: aastore
        23: dup
        24: iconst_1
        25: dload_2
        26: invokestatic   #8         // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
        29: aastore
        30: invokevirtual  #9         // Method java/io/PrintStream.format:(Ljava/lang/↙
    ↘ String;[Ljava/lang/Object;]Ljava/io/PrintStream;
        33: pop
        34: return

可见,int型数据和double型数据首先经由各自的valueOf方法处理、返回相应的数据值。format()方法的输入值应当为Object型实例。而Integer和Double类是超类Object的子类,所以这种实例可以作为format()函数参数里的数组元素。另外一方面,所有数组都是均质的,也就是说它不能保存不同类型的数据元素,因此int和double类的数据类型不可能是超类数组以外任何类型数组的数据元素。

偏移量为13的指令构造了一个Object型的数组实例,而偏移量为22、29的指令,分别把整型Integer对象,和双精度Double型对象添加到超类对象的数组里。

整个程序的倒数第二行指令pop、即清除了栈顶TOS中的元素数值,因此在执行最后一个指令return时,该方法的数据栈已经被彻底释放(又称作平衡/balanced)。

54.13.6 二维数组

在Java中,二维数组其实就是一个存储着另一维度数组引用指针的一维数组。

        public static void main(String[] args)
        {
                int[][] a = new int[5][10];
                a[1][2]=3;
        }
 
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: iconst_5
         1: bipush          10
         3: multianewarray  #2, 2            // class "[[I"
         7: astore_1
         8: aload_1
         9: iconst_1
        10: aaload
        11: iconst_2
        12: iconst_3
        13: iastore
        14: return

为了展示效果,我们在这里创建一个大小为10×5的整型二维数组,采用的指令是new int[5][10]。

Java采用multinewarray指令构造多维数组。本例先通过iconst_5和bipush指令将各纬度的长度值推送入栈,再使用multinewarray指令声明数据类型(常量解析池#2)和数组维度(2)。

偏移量为9、10的iconst_1和aaload指令用于加载第1行的引用指针。偏移量为11的iconst_2指令则声明了指定列。偏移量为12的指令明确该元素的取值。偏移量为13的iastore指令最终完成元素赋值。

二维数组的读取操作又是如何实现的呢?

        public static int get12 (int[][] in)
        {
                return in[1][2];
        }
 
  public static int get12(int[][]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: iconst_1
         2: aaload
         3: iconst_2
         4: iaload
         5: ireturn

从这个程序我们可以看到:偏移量为2的aaload指令读入了指定行的引用指针,而偏移量为3的iconst_2指令声明了列编号。最终iaload指令读取了指定元素的数值。

54.13.7 三维数组

三维数组可视为存储了二维数组引用指针的一维数组。

        public static void main(String[] args)
        {
                int[][][] a = new int[5][10][15];

                a[1][2][3]=4;

                get_elem(a);
        }
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: iconst_5
         1: bipush          10
         3: bipush          15
         5: multianewarray  #2, 3            // class "[[[I"
         9: astore_1
        10: aload_1
        11: iconst_1
        12: aaload
        13: iconst_2
        14: aaload
        15: iconst_3
        16: iconst_4
        17: iastore
        18: aload_1
        19: invokestatic    #3            // Method get_elem:([[[I]I
        22: pop
        23: return

我们这里举的例子中,三个维度的数值分别是5、10以及15。

它需要使用两次aaload指令才能找到最后一维数组的引用指针。

        public static int get_elem (int[][][] a)
        {
                return a[1][2][3];
        }
  public static int get_elem(int[][][]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: iconst_1
         2: aaload
         3: iconst_2
         4: aaload
         5: iconst_3
         6: iaload
         7: ireturn
54.13.8 小结

在Java中,是否可能发生缓冲区溢出的情况?不可能。Java数组的数据实例存储了数组长度的明确信息,数组操作会作边界检查。一旦发生上标/下标溢出问题,运行环境就会进行异常处理。

Java和C/C++的多维数组在底层结构上存在显著的区别,因此JAVA不太适合用进行大规模科学计算。

54.14 字符串

54.14.1 第一个例子

Java的字符串和数组都是同等对象,因此它们的构造过程没有什么区别。

        public static void main(String[] args)
        {
               System.out.println("What is your name?");
               String input = System.console().readLine();
               System.out.println("Hello, "+input);
        }
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: getstatic      #2        // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc            #3        // String What is your name?
         5: invokevirtual  #4        // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: invokestatic   #5        // Method java/lang/System.console:()Ljava/io/Console;
        11: invokevirtual  #6        // Method java/io/Console.readLine:()Ljava/lang/String;
        14: astore_1
        15: getstatic      #2        // Field java/lang/System.out:Ljava/io/PrintStream;
        18: new            #7        // class java/lang/StringBuilder
        21: dup
        22: invokespecial  #8        // Method java/lang/StringBuilder."<init>":()V
        25: ldc            #9        // String Hello,
        27: invokevirtual  #10       // Method java/lang/StringBuilder.append:(Ljava/↙
↘ lang/String;)Ljava/lang/StringBuilder;
        30: aload_1
        31: invokevirtual  #10       // Method java/lang/StringBuilder.append:(Ljava/↙
↘ lang/String;)Ljava/lang/StringBuilder;
        34: invokevirtual  #11       // Method java/lang/StringBuilder.toString:()↙
↘ Ljava/lang/String;
        37: invokevirtual  #4        // Method java/io/PrintStream.println:(Ljava/lang↙
↘ /String;)V
        40: return

我们这里举的例子是交互式的,具体功能是能根据用户输入的用户名,显示一句问候,作为回显结果。

偏移量为11的指令调用了readLine函数。其返回值,即用户输入的字符串的引用指针,最后通过栈顶TOS返回。偏移量为14的指令将字符串的引用指针存储在LVA的第一个存储单元中。偏移量为30的指令再次加载了用户输入字符串的引用指针,在StringBuilder类的实例中与字符串(Hello, )连接为新的字符串。最后偏移量为37的invokevirtual指令调用了println方法,显示最终的字符串。

54.14.2 第二个例子

另外一个例子是:

public class strings
{
        public static char test (String a)
        {
                return a.charAt(3);
        };

        public static String concat (String a, String b)
        {
                return a+b;
        }
}
  public static char test(java.lang.String);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: iconst_3
         2: invokevirtual #2            // Method java/lang/String.charAt:(I)C
         5: ireturn

编译器会利用StringBuilder类来连接字符串:

  public static java.lang.String concat(java.lang.String, java.lang.String);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: new            #3            // class java/lang/StringBuilder
         3: dup
         4: invokespecial  #4            // Method java/lang/StringBuilder."<init>":()V
         7: aload_0
         8: invokevirtual  #5            // Method java/lang/StringBuilder.append:(Ljava/ ↙
    ↘ lang/String;)Ljava/lang/StringBuilder;
        11: aload_1
        12: invokevirtual  #5            // Method java/lang/StringBuilder.append:(Ljava/ ↙
    ↘ lang/String;)Ljava/lang/StringBuilder;
        15: invokevirtual  #6            // Method java/lang/StringBuilder.toString:() ↙
    ↘ Ljava/lang/String;
        18: areturn

我们再看一个将字符串和整型数连接在一起的例子:

        public static void main(String[] args)
        {
                String s="Hello!";
                int n=123;
                System.out.println("s=" + s + " n=" + n);
        }

这里同样调用了StringBuilder类的append方法连接字符串,再通过println函数显示最终的字符串。

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: ldc            #2            // String Hello!
         2: astore_1
         3: bipush         123
         5: istore_2
         6: getstatic      #3            // Field java/lang/System.out:Ljava/io/PrintStream;
         9: new            #4            // class java/lang/StringBuilder
        12: dup
        13: invokespecial  #5            // Method java/lang/StringBuilder."<init>":()V
        16: ldc            #6            // String s=
        18: invokevirtual  #7            // Method java/lang/StringBuilder.append:(Ljava/ ↙
    ↘ lang/String;)Ljava/lang/StringBuilder;
        21: aload_1
        22: invokevirtual  #7            // Method java/lang/StringBuilder.append:(Ljava/ ↙
    ↘ lang/String;)Ljava/lang/StringBuilder;
        25: ldc            #8            // String n=
        27: invokevirtual  #7            // Method java/lang/StringBuilder.append:(Ljava/ ↙
    ↘ lang/String;)Ljava/lang/StringBuilder;
        30: iload_2
        31: invokevirtual  #9            // Method java/lang/StringBuilder.append:(I)Ljava ↙
    ↘ /lang/StringBuilder;
        34: invokevirtual  #10            // Method java/lang/StringBuilder.toString:() ↙
    ↘ Ljava/lang/String;
        37: invokevirtual  #11            // Method java/io/PrintStream.println:(Ljava/lang ↙
    ↘ /String;)V
        40: return

54.15 异常处理

让我们再来回顾一下54.13.4节中已经讲到的例子,那是一个关于月份显示的程序实例。显然如果输入数组的数值小于0或者大于11的话,都会触发异常处理函数。

指令清单54.10 IncorrectMonthException.java(不正确的月份显示例外)

public class IncorrectMonthException extends Exception
{
        private int index;

        public IncorrectMonthException(int index)
        {
                this.index = index;
        }
        public int getIndex()
        {
                return index;
        }
}

指令清单54.11 Month2.java(月份2)

class Month2
{
        public static String[] months =
        {
                "January",
                "February",
                "March",
                "April",
                "May",
                "June",
                "July",
                "August",
                "September",
                "October",
                "November",
                "December"
        };

        public static String get_month (int i) throws IncorrectMonthException
        {
                if (i<0 || i>11)
                        throw new IncorrectMonthException(i);
                return months[i];
        };

        public static void main (String[] args)
        {
                try
                {
                        System.out.println(get_month(100));
                }
                catch(IncorrectMonthException e)
                {
                        System.out.println("incorrect month index: "+ e.getIndex());
                        e.printStackTrace();
                }
        };
}

本质上讲,IncorrectMonthException.class只具备一个对象构造函数和一个访问器。

这个类由Exception继承而来,因此它首先调用Exception类的构造函数,接着声明了自己唯一的输入值字段。

  public IncorrectMonthException(int);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokespecial  #1            // Method java/lang/Exception."<init>":()V
         4: aload_0
         5: iload_1
         6: putfield       #2            // Field index:I
         9: return

而getIndex()就是一个访问器/accessor。它通过aload_0指令从LVA的第0个存储槽获取IncorrectMonthException的this指针,再通过getfield指令从对象实例里提取整数值。

  public int getIndex();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield         #2            // Field index:I
         4: ireturn

现在让我们来看看Month2.class中的get_month()。

指令清单54.12 Month2.class

  public static java.lang.String get_month(int) throws IncorrectMonthException;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: iload_0
         1: iflt           10
         4: iload_0
         5: bipush         11
         7: if_icmple      19
        10: new            #2            // class IncorrectMonthException
        13: dup
        14: iload_0
        15: invokespecial  #3            // Method IncorrectMonthException."<init>":(I)V
        18: athrow
        19: getstatic      #4            // Field months:[Ljava/lang/String;
        22: iload_0
        23: aaload
        24: areturn

我们来分析分析这个程序:

在偏移为1的iflt指令在栈顶值小于1的情况下触发跳转。“iflt”是英文if less than的缩写。

当index参数是无效值时,程序会跳转到偏移量为10的new指令,创建一个新的对象。而该对象的类型就是指令的操作数(常量解析池#2)IncorrectMonthException。接着偏移量为15的指令调用构造函数,并通过栈顶TOS传递局部变量index。当执行到偏移量为18的指令处时,异常处理实例已经构造完毕,athrow指令将从栈顶提取由上一条指令传递的异常处理方法的引用指针,并通知JVM系统该方法为当前类实例的异常处理函数。

此处的athrow指令并不返回控制流。此后的偏移量为19的指令开始是另外一个基本的模块,它与异常处理过程没有关系,可视为从偏移量为7的指令开始的领悟一个逻辑分支。

例外的句柄是如何工作的?我们来看看类Month2.class中的函数main()。

指令清单54.13 Month2.class

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: getstatic      #5        // Field java/lang/System.out:Ljava/io/PrintStream;
         3: bipush 100
         5: invokestatic   #6        // Method get_month:(I)Ljava/lang/String;
         8: invokevirtual  #7        // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        11: goto 47
        14: astore_1
        15: getstatic      #5        // Field java/lang/System.out:Ljava/io/PrintStream;
        18: new            #8        // class java/lang/StringBuilder
        21: dup
        22: invokespecial  #9        // Method java/lang/StringBuilder."<init>":()V
        25: ldc            #10       // String incorrect month index:
        27: invokevirtual  #11       // Method java/lang/StringBuilder.append:(Ljava/ ↙
    ↘ lang/String;)Ljava/lang/StringBuilder;
        30: aload_1
        31: invokevirtual  #12       // Method IncorrectMonthException.getIndex:()I
        34: invokevirtual  #13       // Method java/lang/StringBuilder.append:(I)Ljava ↙
    ↘ /lang/StringBuilder;
        37: invokevirtual  #14       // Method java/lang/StringBuilder.toString:() ↙
    ↘ Ljava/lang/String;
        40: invokevirtual  #7        // Method java/io/PrintStream.println:(Ljava/lang ↙
    ↘ /String;)V
        43: aload_1
        44: invokevirtual  #15       // Method IncorrectMonthException.printStackTrace ↙
:()V
        47: return
      Exception table:
        from      to target type
            0    11    14   Class IncorrectMonthException

自偏移量为14的指令开始的内容就是异常表Exception table。在程序从偏移量0运行到偏移量11(含)期间,发生的全部异常状况都会交给IncorrectMonthException处理。当输入值为无效值时,程序流向导至偏移量为14的指令。实际上,主程序在偏移量为11的地方就已经结束。正常情况下,程序不会执行到偏移量为14的指令,而且也没有任何条件转移指令或者无条件转移指令会跳转到该处。只有当程序遇到例外情况时,程序才运行到偏移量大于11的指令。异常处理的第一条位于偏移量14。此处的astore_1会把外部传入的、异常处理实例的引用指针存储到LVA的第一个存储槽。在此之后,偏移量为31的指令将会通过这个引用指针调用异常处理实例的getIndex()方法。此时偏移量为30的指令把这个引用指针已经提取出来了。异常表的其他指令都是字符串处理指令:getIndex()方法返回局部变量index的整数值,这个值由toString()方法转换为字符串,再与字符串“incorrect month index:”连接,最终通过println()和printStackTrace()方法显示出来。在调用了printStackTrace()之后,整个异常处理过程宣告完毕,程序恢复正常状态。虽然本例偏移量位47的指令是结束main()函数的return指令,但是此处可以是其他的、在正常状态下需要执行的任何指令。

接下来,我们来看看IDA显示异常处理方法的具体方式。

指令清单54.14 笔者计算机里某个class文件的异常处理方法

      .catch java/io/FileNotFoundException from met001_335 to met001_360\
  using met001_360
      .catch java/io/FileNotFoundException from met001_185 to met001_214\
  using met001_214
      .catch java/io/FileNotFoundException from met001_181 to met001_192\
  using met001_195
      .catch java/io/FileNotFoundException from met001_155 to met001_176\
  using met001_176
      .catch java/io/FileNotFoundException from met001_83 to met001_129 using \
met001_129
      .catch java/io/FileNotFoundException from met001_42 to met001_66 using \
met001_69
      .catch java/io/FileNotFoundException from met001_begin to met001_37\
  using met001_37

54.16 类

一个简单的类如下所示。

指令清单54.15 test.java

public class test
{
        public static int a;
        private static int b;

        public test()
        {
            a=0;
            b=0;
        }
        public static void set_a (int input)
        {
                a=input;
        }
        public static int get_a ()
        {
                return a;
        }
        public static void set_b (int input)
        {
                b=input;
        }
        public static int get_b ()
        {
                return b;
        }
}

构造函数把两个变量设置为0:

  public test();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial  #1            // Method java/lang/Object."<init>":()V
         4: iconst_0
         5: putstatic      #2            // Field a:I
         8: iconst_0
         9: putstatic      #3            // Field b:I
        12: return

设置a

  public static void set_a(int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: iload_0
         1: putstatic      #2            // Field a:I
         4: return

获取a

  public static int get_a();
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic      #2            // Field a:I
         3: ireturn

设置b

  public static void set_b(int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: iload_0
         1: putstatic        #3            // Field b:I
         4: return

获取b

  public static int get_b();
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic        #3            // Field b:I
         3: ireturn

在底层指令层面上,类中那些具有public和private属性的成员对象没有实质区别。但是.class文件级别,外部指令无法直接访问其他类里的private属性成员。

接下来,我们演示创建对象和调用方法。

指令清单54.16 ex1.java程序

public class ex1
{
        public static void main(String[] args)
        {
                test obj=new test();
                obj.set_a (1234);
                System.out.println(obj.a);
        }
}
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new            #2            // class test
         3: dup
         4: invokespecial  #3            // Method test."<init>":()V
         7: astore_1
         8: aload_1
         9: pop
        10: sipush 1234
        13: invokestatic   #4            // Method test.set_a:(I)V
        16: getstatic      #5            // Field java/lang/System.out:Ljava/io/PrintStream;
        19: aload_1
        20: pop
        21: getstatic      #6            // Field test.a:I
        24: invokevirtual  #7            // Method java/io/PrintStream.println:(I)V
        27: return

new指令可以创建新的对象,但是它并没有调用构造函数(偏移量为4的指令调用了构造函数)。偏移量为13的指令调用了set_a()方法。偏移量为21的getstatic指令访问了类的一个字段。

54.17 简单的补丁

54.17.1 第一个例子

本节通过一个简单的程序演示补丁的实现方法:

public class nag
{
        public static void nag_screen()
        {
                System.out.println("This program is not registered");
        };
        public static void main(String[] args)
        {
                System.out.println("Greetings from the mega-software");
                nag_screen();
        }
}

我们可否去掉字符串“This program is not registered”?

我们使用调试工具IDA加载类文件.class,如图54.1所示。

5401{}

图54.1 IDA

如图54.2所示,我们试图把该函数的第一个字节改为177,即return的字节码。

5402{}

图54.2 IDA

但是如此一来程序就崩溃了(运行环境为JRE 1.7):

Exception in thread "main" java.lang.VerifyError: Expecting a stack map frame
Exception Details:
  Location:
    nag.nag_screen()V @1: nop
  Reason:
    Error exists in the bytecode
  Bytecode:
    0000000: b100 0212 03b6 0004 b1

        at java.lang.Class.getDeclaredMethods0(Native Method)
        at java.lang.Class.privateGetDeclaredMethods(Class.java:2615)
        at java.lang.Class.getMethod0(Class.java:2856)
        at java.lang.Class.getMethod(Class.java:1668)
        at sun.launcher.LauncherHelper.getMainMethod(LauncherHelper.java:494)
        at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:486)

也许,JVM还存在某种与栈有关的检查机制。

好吧,我们采用一个其他的方法来“打补丁”:直接覆盖nag()的调用指令,如图54.3所示。

5403{}

图54.3 IDA

0就是NOP的字节码。

经过运行检验,这次的“打补丁”是成功的。

54.17.2 第二个例子

下面我们再看看另外一个简单的例子:

public class password
{
        public static void main(String[] args)
        {
                System.out.println("Please enter the password");
                String input = System.console().readLine();
                if (input.equals("secret"))
                        System.out.println("password is correct");
                else
                        System.out.println("password is not correct");
        }
}

其实现的基本思路是按照提示输入密码字符串,当输入的密码字符串为“secret”时,显示字符串密码正确(password is correct);否则显示字符串密码不正确(password is not correct)。

将该程序调入到调试工具IDA中,如图54.4所示。

5404{}

图54.4 IDA

关键之处是比较字符串的ifeq指令。这个指令其实是英文if equal(如果相等)的缩写。实际上这个助记符不太贴切,它要是ifz(也就是如果TOS是零)就更加确切了。也就是说,如果栈顶TOS的值是零,它就进行跳转。在这个例子中,只有当输入的密码有误才会触发跳转(布尔“假”/False的对应值是0)。我们的第一个想法就是调整这个指令。在ifeq的字节码里,有两个字节专门封装转移目标地址的偏移量。要想把它强行改为“无条件不转移”,必须把第三个字节的改为3(ifeq占用3个字节,PC的偏移量加3就是执行下一条指令):

我们把相应指令改为如图54.5所示的样子 。

5405{}

图54.5 IDA

结果,修改后的程序无法在JRE 1.7环境下正确执行。

Exception in thread "main" java.lang.VerifyError: Expecting a stackmap frame at branch target 24
Exception Details:
  Location:
    password.main([Ljava/lang/String;]V @21: ifeq
  Reason:
    Expected stackmap frame at this location.
  Bytecode:
    0000000: b200 0212 03b6 0004 b800 05b6 0006 4c2b
    0000010: 1207 b600 0899 0003 b200 0212 09b6 0004
    0000020: a700 0bb2 0002 120a b600 04b1
  Stackmap Table:
    append_frame(@35,Object[#20])
    same_frame(@43)

        at java.lang.Class.getDeclaredMethods0(Native Method)
        at java.lang.Class.privateGetDeclaredMethods(Class.java:2615)
        at java.lang.Class.getMethod0(Class.java:2856)
        at java.lang.Class.getMethod(Class.java:1668)
        at sun.launcher.LauncherHelper.getMainMethod(LauncherHelper.java:494)
        at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:486)

但是在JRE 1.6版本下,如此修改的程序的确可以正常运行。

笔者也尝试了“将这个ifeq指令的字节码直接更换成3个空指令NOP”的做法。即使是这样,修改后的程序仍然不能正常运行。大概是JRE 1.7的栈映射核查更为全面吧!

接下来,我们更换一种方法:把ifeq之前的、调用input.equals方法的全部指令全都替换为NOP,把它改为如图54.6所示的样子。

5406{}

图54.6 IDA

如此修改之后,在执行到ifeq指令的时候栈顶的值永远是1,因此不会满足ifeq的跳转条件。

试验说明,这种方法果然有效。

54.18 总结

和C/C++语言相比,Java语言少了些什么数据类型?

① 结构:采用类。

② 联合:采用类继承。

③ 无符号数据类型:这也直接导致了在JAVA下,实现密码算法比较困难。

④ 函数指针。


[1] 关于常规指针和引用指针的详细区别可以查阅本书的51.3节。