视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
java 类与构造方法
2025-09-25 14:20:58 责编:小OO
文档
关键字: java 构造 javap 字节码 bytecode 

  按照java规范,一个类实例的构造过程是遵循以下顺序的: 

  1.如果构造方法(constructor,也有翻译为构造器和构造函数的)是有参数的则进行参数绑定。 

  2.内存分配将非静态成员赋予初始值(原始类型的成员的值为规定值,例如int型为0,float型为0.0f,boolean型为false;对象类型的初始值为null),静态成员是属于类对象而非类实例,所以类实例的生成不进行静态成员的构造或初始化,后面将讲述静态成员的生成时间。 

  3.如果构造方法中存在this()调用(能是其他带参数的this()调用)则执行之,执行完毕后进入第6步继续执行,如果没有this调用则进行下一步。 

  4.执行显式的super()调用(能是其他带参数的super()调用)或隐式的super()调用(缺省构造方法),此步骤又进入一个父类的构造过程并一直上推至object对象的构造。 

  5.执行类申明中的成员赋值和初始化块。 

  6.执行构造方法中的其他语句。 

  目前来看看精心构造的一个实例: 

  class parent 

  { 

  int pm1; 

  int pm2=10; 

  int pm3=pmethod(); 

  { 

  system.out.println("parents instance initialize block"); 

  } 

  public static int spm1=10; 

  static 

  { 

  system.out.println("parents static initialize block"); 

  } 

  parent() 

  { 

  system.out.println("parents default constructor"); 

  } 

  static void staticmethod() 

  { 

  system.out.println("parents staticmethod"); 

  } 

 int pmethod() 

  { 

  system.out.println("parents method"); 

  return 3; 

  } 

  } 

  class child extends parent 

  { 

  int cm1; 

  int cm2=10; 

  int cm3=cmethod(); 

  other co; 

  public static int scm1=10; 

  { 

  system.out.println("childs instance initialize block"); 

  } 

  static 

  { 

  system.out.println("childs static initialize block"); 

  } 

   

  child() 

  { 

  co=new other(); 

  system.out.println("childs default constructor"); 

  } 

  child(int m) 

  { 

  this(); 

  cm1=m; 

  system.out.println("childs self-define constructor"); 

  } 

  static void staticmethod() 

  { 

  system.out.println("childs staticmethod"); 

  } 

  int cmethod() 

  { 

  system.out.println("childs method"); 

  return 3; 

  } 

  } 

  class other 

  { 

  int om1; 

  other() { 

  system.out.println("others default constructor"); 

  } 

  } 

 public class initializationtest 

  { 

  public static void main(string args[]) 

  { 

  child c; 

  system.out.println("program start"); 

  system.out.println(child.scm1); 

  c= new child(10); 

  system.out.println("program end"); 

  } 

  } 

  进入此文件所在的目录,然后 

  编译此文件:javac initializationtest.java 

  运行此程式:java ?classpath . initializationtest 

  得到的结果是: 

  program start 

  parents static initialize block 

  childs static initialize block 

  10 

  parents method 

  parents instance initialize block 

  parents default constructor 

  childs method 

  childs instance initialize block 

  others default constructor 

  childs default constructor 

  childs self-define constructor 

  program end 

 如果没有看过上面的关于类的构造的说明,非常容易让人误解为类的构造顺序是如下的结果(忽略参数绑定、内存分配和非静态成员的缺省值赋值): 

  1.完成父类的非静态成员初始化赋值及执行初始化块(这个的先后顺序取决于源文件中的书写顺序,能将初始化块置于成员声明前,那么先执行的将是初始化块,将上面的代码稍稍变动一下就能验证这一点。) 

 2.调用父类的构造方法完成父类构造。 

 3.完成非静态成员的初始化赋值及执行初始化块。 

  4.调用构造方法完成对象的构造,执行构造方法体中的其他内容。 

  如果根据以上java规范中给出的顺序也能合理的解释程式的输出结果,那么怎么亲眼看到是规范中的顺序而不是以上根据程式的输出推断的顺序呢? 

  下面就使用jdk自带的javap工具看看实际的顺序,这个工具是个根据编译后的字节码生成一份字节码的助记符格式的文件的工具,就像根据机器码生成汇编代码那样。 

反编译:javap -c -classpath . child 

  输出的结果是(已经过标记,交替使用黑体和斜体表示要讲解的每一块): 

  compiled from initializationtest.java 

  class child extends parent { 

  int cm1; 

  int cm2; 

  int cm3; 

  other co; 

  public static int scm1; 

  static {}; 

  child(); 

  child(int); 

  int cmethod(); 

  static void staticmethod(); 

  } 

 method static {} 

  0 bipush 10 

  2 putstatic #22

  5 getstatic #20

  8 ldc #5

  10 invokevirtual #21

  13 return 

  method child() 

  0 aload_0 

  1 invokespecial #14

  4 aload_0 

  5 bipush 10 

  7 putfield #16

  10 aload_0 

  11 aload_0 

  12 invokevirtual #18

  15 putfield #17

  18 getstatic #20

  21 ldc #2

  23 invokevirtual #21

  26 aload_0 

  27 new #8

  30 dup 

  31 invokespecial #13

  34 putfield #19

  37 getstatic #20

  40 ldc #1

  42 invokevirtual #21

  45 return 

  method child(int) 

  0 aload_0 

  1 invokespecial #12

  4 aload_0 

  5 iload_1 

6 putfield #15

  9 getstatic #20

  12 ldc #4

  14 invokevirtual #21

  17 return 

  method int cmethod() 

  0 getstatic #20

  3 ldc #3

  5 invokevirtual #21

  8 iconst_3 

  9 ireturn 

  method void staticmethod() 

  0 getstatic #20

  3 ldc #6

  5 invokevirtual #21

  8 return 

  请仔细浏览一下这个输出并和原始码比较一下。 

  下面解释怎么根据这个输出得到类实例的实际的构造顺序,在开始说明前先解释一下输出的语句的格式,语句中最前面的一个数字是指令的偏移值,这个我们在此能不管,第二项是指令助记符,能从字面上大致看出指令的意思。 

  例如 getstatic 指令将一个静态成员压入一个称为操作数堆栈(后续的指令就能引用这个数据结构中的成员)的数据结构,而 invokevirtual 指令是调用java虚拟机方法,第三项是操作数(#号后面跟一个数字,实际上是类的成员的标记),有些指令没有这一项,因为有些指令如同汇编指令中的某些指令相同是不必操作数的(可能是操作数是隐含的或根本就不必),这是java中的一个特色。 

  如果你直接检查字节码,你会看到成员信息没有直接嵌入指令而是像所有由java类使用的常量那样存储在一个共享池中,将成员信息存储在一个常量池中能减小字节码指令的大小,因为指令只需要存储常量池中的一个索引而不是整个常量。 

  需要说明的是常量池中的项目的顺序是和编译器相关的,因此在你的环境中看到的可能和我上面给出的输出不完全相同,第四项是对前面的操作数的说明,实际的字节码中也是没有的,根据这个你能非常清晰的得到实际上使用的是哪个成员或调用的是哪个方法,这也是javap为我们提供的便利。 

  说完上面这些你目前应该非常容易看懂上面的结果和下面将要叙述的内容了。其他更进一步的有关java字节码的信息请自己查找资料。 

 先看看最开始的部分,非常像一个标准的c++类的声明,确实如此。成员声明的后面没有了成员初始化赋值语句和初始化块,那么这些语句何时执行的呢?先不要急,继续往下看。 

  第二块,是个method static {},对比看看第一部分,他被处理为一个静态的方法(从前面的method能看出),这就是原始码中的静态初始化块,从后面的语句能看出他执行的就是system.out.println("childs static initialize block")语句,由于这个方法是没有方法名的,所以他不能被显式的调用,他在何处调用后面会有叙述。 

第三块,缺省构造方法的实现,这是本文的重点,下面周详讲解。由于原始码中的缺省构造方法没有显式调用this方法,因此没有this调用(对比看看下一块的有参的构造方法的前两句),同时也没有显式的super调用,那么隐式调用父类的缺省构造方法,也就是前两条语句(主要是语句invokespecial #14 ),他调用父类的构造方法,和这个类的构造相似(你能使用javap ?c ?classpath . parent反编译父类的字节码看看这个类的构造过程);紧接着的是执行原始码中的第一条初始化赋值语句cm2=10(即接下来的三条语句,主要是bipush 10和putfield #15 ,此处回答了第一块中的疑问,即初始化赋值语句到哪儿去了。);接下来是执行cm3=cmethod()(接下来的四条语句);然后是执行初始化块中的内容system.out.println("childs instance initialize block")(接下来的三条语句);java规范内部约定的内容至此执行完毕,开始执行构造方法的方法体中的内容,即co=new other()(接下来的五条语句)和system.out.println("childs default constructor")(接下来的三条语句),最后方法执行完毕返回(最后一条语句return)。 

  剩下的几块相信应该不用解释了吧,有参构造方法调用无参构造方法然后执行自己的方法体,成员方法cmethod执行一条打印语句然后返回一个常量3,静态方法staticmethod执行一条打印语句。 

  另外需要说明一下的是你能将有参构造方法中的this调用去掉,然后看看反编译的结果,你会发现两个构造方法非常的类似,如果你将两个构造方法的内容改为相同的,那么反编译后的生成也将是同样的。从这个能说明本文开始的构造顺序的说明中构造方法中this调用的判断是在编译阶段就完成的,而不是在运行阶段(说明中的意思似乎是这个判断是在运行时进行的)。 

  对构造过程的另一个细节你可能还不相信,就是顺序中的第二条关于非静态成员的赋予缺省初始值(内存分配部分无法考证,这是java虚拟机自动完成的),这个你能通过在子类child的cmethod方法的最开始用 system.out.println(cm3)打印cm3的值(输出为0,其他类型成员的值能通过类似的方法得到)。 

  下面来讲解另一个还没有解决的问题:静态成员初始化和静态初始化块的执行是在何时完成的?这个能通过一个小小的试验推断得到:是在第一次使用该类对象时进行的(注意是类对象而不是类实例,对于类的公有静态成员能直接通过类名进行访问,并不必生成一个类实例,这就是一次类对象的使用而非类实例的使用,如果在生成第一个类实例前没有使用过该类对象,那么在构造第一个类实例前先完成类对象的构造(即完成静态成员初始化及执行静态初始化块),然后再执行以上类实例的构造过程),试验的步骤如下: 

  1.修改main方法,将其中的system.out.println(child.scm1)和c= new child(10)都注释掉(不要删除,后面还需要用到这两个语句),编译运行程式,输出将只有program start和program end,这说明没有使用类对象也没有生成类实例时不进行静态成员的构造。 

  2.将system.out.println(child.scm1)的注释取消,编译运行后输出多了父类和子类的静态初始化块部分的执行输出(使用子类的类对象将导致生成父类的类对象,父类先于子类构造)。 

  3.将system.out.println(child.scm1)注释掉并取消c= new child(10)的注释,编译运行后输出只比最开始没有注释所有语句时少了一条(输出child.scm1的值10)下载本文

显示全文
专题