【疯狂Java_突破程序员基本功的16课】charpt2 对象与内存控制
2.1.2 实例变量的初始化时机
JDK里面提供了一个叫做javap的工具,主要用于帮助开发者深入了解Java编译器的机制,其语法格式如下:
javap <options> <classes>...
该工具支持如下的常用选项:
-c:分解方法代码,也就是显示每个方法的具体的字节码。
-l:用于指定显示行号和局部变量列表
-public | protected | package | private :用于指定显示哪种级别的类成员,分别对应Java的4中访问控制权限。
-verbose:用于指定显示更进一步的详细信息。
定 义实例变量时指定的初始值、初始化块中为实例变量指定的初始值、构造器中为实例变量指定的初始值,三这的作用完全类似,都用于对实例变量指定初始值。经过 编译器处理后,他们对应的赋值语句都被合并到构造器中。在合并过程中,定义变量语句转换得到的赋值语句、初始化块里面的语句转换得到的赋值语句,总是位于 构造器的所有语句之前,合并后,两种赋值语句的顺序保持它们在源代码中的顺序。
2.1.3类变量的初始化时机
看一道面试题:写出以下程序的输出
class Price{ final static Price INSTANCE = new Price(2.8); static double initPrice = 20; double currentPrice; public Price(double discount){ currentPrice = initPrice - discount; } } public class PriceTest{ public static void main(String[] args){ System.out.println(Price.INSTANCE.currentPrice); //1 Price p = new Price(2.8); System.out.println(p.currentPrice); //2 } }
分析:
程 序中1、2行代码都访问到Price实例的currentPrice实例变量,而且程序都是通过new Price(2.8);来创建Price实例的。 表明上看,程序输出两个Price的currentPrice都应该返回17.2,但实际上程序并没有输出两个17.2,而是输出了-2.8和17.2.
下面从内存角度来分析这个程序。第一次用到Price类时,程序开始对Price类进行初始化,初始化分成以下两个阶段:
a、系统为Price的两个类变量分配内存空间。
b、按初始化代码的排列顺序对类变量执行初始化。
在 a阶段,系统先为INSTANCE、initPrice两个变量分配内存空间,此时INSTANCE、initPrice的值为默认值null和0.0. 接着初始化进入b阶段,程序按顺序依次为INSTANCE和initPrice进行赋值。对INSTANCE赋值时要调用Price(2.8),创建 Price实例,此时立即执行代码:currentPrice = initPrice - discount;为currentPrice进行赋值,此时initPrice类变量的值还是默认值0.0,因此赋值的结果是currentPrice等于-2.8.接着,程序再将initPrice赋值为 20,但此时对INSTANCE的currentPrice的实例变量已经不起作用了。
当Price类初始化完成后 INSTANCE类变量引用到一个currentPrice为-2.8的Price实例,而initPrice的类变量值为20.0.以后再创建 Price实例时,currentPrice的实例变量的值才等于20.0-discount
2.2父类构造器
super 用以显式的调用父类的构造器,this用以显示调用本类中的另一个重载的构造器。super调用和this调用都只能在构造器中使用,而且都只能出现在构 造器中的第一行代码上,所以这也意味着一个构造器中不可能同时出现super和this调用,而且最多只能调用一次。
如果子类的构造器中没有显示的调用父类的构造器,则会默认调用父类的无参的构造器。
在执行构造器代码之前,对象所占的内存就已经被分配下来,这些内存值都默认是空值——基本类型的变量,默认值就是0或false,引用类型的变量,默认值就是null。
如果父类的构造器中调用了被子类重写的方法,且通过子类构造器来创建子类对象,调用(显式或隐示)了这个父类构造器,就会导致子类的重写方法在子类构造器的所有代码之前被执行,也就是此时子类的实例变量还是默认值,没有被赋予初始值。
2.3父类构造器
2.3.1继承成员变量和成员方法的区别
编译器在处理成员变量和成员方法时有这样的区别:成员变量仍然保存在父类中,而成员方法则会转移到子类中(如果子类重写了该方法,则是覆写后的方法)。如果子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。对于实例变量,即使子类中定义了与父类完全同名的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量。
对于一个引用类型的变量而言,当通过改变量访问它所引用的对象的实例变量时,该实例变量的值取决于声明该变量时类型;当通过该变量来调用它所引用对象的方法时,该方法的行为取决于它所实际引用的对象的类型。
参见以下的示例程序:
public class FiledMethodOverride extends ParentClass { public int i = 100; public void print() { System.out.println("child:" + i); } public static void main(String[] args) { ParentClass pc = new FiledMethodOverride(); System.out.println(pc.i); //输出:0 pc.print(); //输出:child:100 } } class ParentClass { public int i = 0; public void print() { System.out.println("parent:" + i); } }
2.3.2内存中子类实例
当创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为其父类中定义的所有实例变量分配内存,即使子类中定义了与父类中同名的实例变量。也就是说,当系统创建一个Java对象的时候,如果该java类有两个直接父类(直接父类A和间接父类B),假设A类中定义了2个实例变量,B类中定义了3个实例变量,当前类中定义了2个实例变量,那这个Java对象将会保存2+3+2个实例变量。
如果在子类中定义了与父类中同名的实例变量,那么子类中的该实例变量将会隐藏父类中定义的同名变量,注意不是完全覆盖,一次系统在创建子类对象时,依然会为父类中定义的、被隐藏的变量分配内存空间的。
为了在子类中访问父类中定义的、被隐藏的实例变量,或者为了在子类方法中调用父类中定义的、被覆盖的方法,可以通过super.作为限定修饰符来修饰这些实例变量和实例方法。
2.3.3父、子类中的类变量
对于父类中的类变量或类方法(静态变量或静态方法),如果在子类中没有与之同名的变量或方法(无论是实例变量或方法还是类变量或方法),则在子类可以直接使用父类的类变量或方法;否则需要通过super.或父类名.来使用父类的类变量或方法
2.4 final修饰符
- final可以修饰变量,被final修饰的变量被赋初值后,不能再对它重新赋值;
- final可以修饰方法,被final修饰的方法不能被重写;
- final可以修饰类,被final修饰的类不能被继承;
2.4.1final修饰的变量
被fianl修饰的实例变量必须被显示的赋初值,而且只能在如下3个位置赋值:
- 定义final变量时赋初值
- 在非静态初始块中为final变量指定初始值
- 在构造器中为final变量指定初始值
对于final修饰的类变量而言,同样也需要被显示的赋初值,而且只能在如下2个位置赋值:
- 定义final类变量时赋初值
- 在静态初始化块中为final类变量指定初始值
2.4.2执行“宏替换”的变量
对于一个使用final修饰的变量,不管它是类变量、实例变量还是局部变量,只要定义该 final变量时就指定其初始值,而且这个初始值在编译时就可以确定下来(例如2、2.3、"字符串常量"),那么这个final变量将不再是一个变量, 系统会将其当成“宏变量”处理。也就是说,编译器会把程序中所有使用到该变量的地方直接替换成该变量的值。
除了那种为final变量赋直接值的情况外,如果被赋的表达式只是基本的算术运算表达式,或字符串直接量,没有访问普通变量或调用方法,编译器同样会将这种final变量当作“宏变量”处理。
2.4.4内部类中的局部变量
如果程序需要在局部内部类(包括匿名内部类)中使用局部变量,那么这个局部变量就必须用final修饰。
java要求局部内部类中中访问的局部变量必须被final修饰符修饰,也是有原因的:对于普通局部变量而言,它的作用域就是停留在该方法内,当方法结束后,该局部变量也会随之消失,但内部类可能产生隐式的闭包,闭包将使得局部变量脱离它所在的方法继续存在。