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

  • 相关阅读:
    搭建自己的SIPserver:开源sipserveropensips的搭建及终端TwInkle的使用
    字符串类习题、面试题具体解释(第二篇)
    【Android笔记】MediaPlayer基本使用方式
    poj 1258 Agri-Net
    ActionScript3游戏中的图像编程(连载二十四)
    SQL中declare申明变量
    CreateFileMapping使用方法
    【剑指offer】调整数组顺序
    2015美团网笔试面试总结(嵌入式/硬件类)(美团网校园招聘)
    linux串口驱动分析
  • 原文地址:https://www.cnblogs.com/yangfengtao/p/2729838.html
Copyright © 2020-2023  润新知