【疯狂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修饰符修饰,也是有原因的:对于普通局部变量而言,它的作用域就是停留在该方法内,当方法结束后,该局部变量也会随之消失,但内部类可能产生隐式的闭包,闭包将使得局部变量脱离它所在的方法继续存在。

posted @ 2012-10-18 17:26  风*依旧  阅读(184)  评论(0编辑  收藏  举报