《疯狂Java:突破程序员基本功的16课》读书笔记-第二章 对象与内存控制

时间:2014-01-14 19:21:47   收藏:0   阅读:789

  Java内存管理分为两个方面:内存分配和内存回收。这里的内存分配特指创建Java对象时JVM为该对象在堆内存中所分配的内存空间。内存回收指的是当该Java对象失去引用,变成垃圾时,JVM的垃圾回收机制自动清理该对象,并回收该对象所占用的内存。由于JVM内置了垃圾回收机制回收失去引用的Java对象所占用的内存,所以很多Java开发者认为Java不存在内存泄漏,资源泄漏的问题。实际上这是一种错觉,Java程序依然会有内存泄漏。

  由于JVM的垃圾回收机制由一条后台线程完成,本身也是非常消耗性能的,因此如果肆无忌惮地创建对象,让系统分配内存,那这些分配的内存都将由垃圾回收机制进行回收。这样做有两个坏处:

(1)       不断分配内存使得系统中可用内存减少,从而降低程序运行性能;

(2)       大量已分配内存的回收使得垃圾回收的负担加重,降低程序的运行性能。

 

2.1实例变量和类变量

  Java程序的变量大体可分为成员变量和局部变量。其中局部变量可分为如下3类:

(1)       形参:在方法签名中定义的局部变量,由方法调用者为其赋值,随方法的结束而消亡。

(2)       方法内的局部变量:在方法内定义的局部变量,必须在方法内对其进行显式初始化。这种类型的局部变量从初始化完成后开始生效,随方法的结束而消亡。

(3)       代码块内的局部变量:在代码块内定义的局部变量,必须在代码块内对其进行显式初始化。这种类型的局部变量从初始化完成后开始生效,随代码块的结束而消亡。

 

  局部变量的作用时间很短暂,他们都被存储在方法的栈内存中。

  类体内定义的变量被成为成员变量(Field)。如果定义该成员变量时没有使用static修饰,该成员变量又被称为非静态变量或实例变量;如果使用了static修饰,则该成员变量又被成为静态变量或类变量。

         (对于static关键字而言,从词义上看来,它是“静态”的意思。但从Java程序的角度来看,static的作用就是将实例成员变成类成员,static只能修饰在类里定义的成员部分,包括成员变量,方法,内部类,初始化块,内部枚举类。如果没有使用static修饰这些类里的成员,这些成员属于该类的实例;如果使用了static修饰,这些成员就属于类本身。从这个意义上看,static只能修饰类里的成员,不能修饰外部类,不能修饰局部变量,局部内部类)。

 

         表面上看,Java类里定义成员变量时没有先后顺序,但实际上Java要求定义成员变量时必须采用合法的前向引用。示例如下:

public class ErrorDef
{
    //下面代码将提示:非法向前引用
    int num1 = num2 + 2;
    int num2 = 20;
}
 
public class ErrorDef2
{
    //下面代码将提示:非法向前引用
    static int num1 = num2 + 2;
    static int num2 = 20;
}

但如果一个是实例变量,一个是类变量,则实例变量总是可以引用类变量,因为类变量的初始化时机总是处于实例变量的初始化时机之前。示例如下:

public class RightDef
{
    //下面代码完全正确
    int num1 = num2 + 2;
    static int num2 = 20;
}

2.1.1 实例变量和类变量的属性

         使用static修饰的成员变量是类变量,属于该类本身;没有使用static修饰的成员变量是实例变量,属于该类的实例。在同一个JVM内,每个类只对应一个Class对象,但每个类可以创建多个Java对象。

         由于同一个JVM内每个类只对应一个Class对象,因此同一个JVM内的一个类的类变量只需一块内存空间;但对于实例变量而言,该类每创建一次实例,就需要为实例变量分配一块内存空间。也就是说,程序中有几个实例,实例变量就需要几块内存空间。

         下面程序可以很好地表现出实例变量属于对象,而类变量属于类的特性

class Person
{
    String name;
    int age;
    static int eyeNum;
    public void info()
    {
        System.out.println("我的名字是:" + name
            + ", 我的年龄是:" + age);
    }
}
public class FieldTest
{
    public static void main(String[] args)
    {
        //类变量属于该类本身,只要该类初始化完成,程序即可使用类变量。
        Person.eyeNum = 2;
        //通过Person类访问eyeNum类变量
        System.out.println("Person的eyeNum属性:" + Person.eyeNum);
        //创建第一个Person对象
        Person p = new Person();
        p.name = "猪八戒";
        p.age = 300;
        //通过p访问Person类的eyeNum类变量
        System.out.println("通过p变量访问eyeNum类变量:" + p.eyeNum);
        p.info();
        //创建第二个Person对象
        Person p2 = new Person();
        p2.name = "孙悟空";
        p2.age = 500;
        p2.info();
        //通过p2修改Person类的eyeNum类变量
        p2.eyeNum = 3;
        //分别通过p、p2和Person访问Person类的eyeNum类变量
        System.out.println("通过p变量访问eyeNum类变量:" + p.eyeNum);
        System.out.println("通过p2变量访问eyeNum类变量:" + p2.eyeNum);
        System.out.println("通过Person类访问eyeNum类变量:" + Person.eyeNum);
    }
}

执行完这段程序,内存情况如图:

bubuko.com,布布扣

当Person类初始化完成后,eyeNum类变量也随之初始化完成。

2.1.2 实例变量的初始化时机

         从程序运行的角度来看,每次创建Java对象都会为实例变量分配内存空间,并对实例变量执行初始化。

         从语法角度来看,程序可以在3个地方对实例变量执行初始化:

(1)       定义实例变量时指定初始值;

(2)       非静态初始化块中对实例变量指定初始值;

(3)       构造器中对实例变量指定初始值。

其中第1,2种方式比第3种方式更早执行,但他们的执行顺序与排列顺序相同。

下面程序示范了实例变量的初始化时机

class Cat
{
    //定义name、age两个实例变量
    String name;
    int age;
    //使用构造器初始化name、age两个实例变量
    public Cat(String name , int age)
    {
        System.out.println("执行构造器");
        this.name = name;
        this.age = age;
    }
    {
        System.out.println("执行非静态初始化块");
        weight = 2.0;
    }
    //定义时指定初始值
    double weight = 2.3;
    public String toString()
    {
        return "Cat[name=" + name
            + ",age=" + age + ",weigth=" + weight + "]";
    }
}
public class InitTest
{
    public static void main(String[] args)
    {
        Cat cat = new Cat("kitty" , 2);
        System.out.println(cat);
        Cat c2 = new Cat("Jerfield" , 3);
        System.out.println(c2);
    }
}

当程序执行Cat cat = new Cat("kitty" , 2);时,创建第一个Cat对象,程序就会先执行Cat类的非静态初始化块,再调用该Cat类的构造器来初始化该Cat实例,内存分配如图:

bubuko.com,布布扣

从上图可以看出,该Cat对象的weight实例变量的值为2.3,而不是初始化块中指定的。这是因为,初始化块中指定初始值,定义weight时指定初始值,都属于对该实例变量执行的初始化操作,它们的执行顺序与它们在源程序中的排列顺序相同。在本程序中,初始化块中对weight的赋值位于定义weight语句之前,因此程序将先执行初始化块中的操作,执行完成后weight的值为2.0;然后再执行定义weight时指定的初始值,完成后weight为2.3,从这里看,初始化块中对weight所指定的初始化值每次都将被2.3所覆盖

 

2.1.3 类变量的初始化时机

         从程序运行的角度来看,每JVM对一个Java类只初始化一次,因此Java程序每运行一次,系统只为类变量分配一次内存空间,执行一次初始化。

         从语法角度来看,程序可以在2个地方对类变量执行初始化:

(1)       定义类变量时指定初始值;

(2)       静态初始化块中对类变量指定初始值

这两种方式的执行顺序与它们在源程序中排列顺序相同。下列程序示范了类变量的初始化时机。

public class StaticInitTest
{
    //定义count类变量,定义时指定初始值。
    static int count = 2;
    //通过静态初始化块为name类变量指定初始值
    static {
        System.out.println("StaticInitTest的静态初始化块");
        name = "Java编程";
    }
    //定义name类变量时指定初始值
    static String name = "疯狂Java讲义";
    public static void main(String[] args)
    {
        //访问该类的两个类变量
        System.out.println("count类变量的值:" + StaticInitTest.count);
        System.out.println("name类变量的值:" + StaticInitTest.name);
    }
}

2.2 父类构造器

         当创建任何Java对象时,程序总会先依次调用每个父类非静态初始化块,父类构造器执行初始化,最后才调用本类的非静态初始化块,构造器执行初始化。

 

2.2.1 隐式调用和显式调用

         当调用某个类的构造器来创建Java对象时,系统总会先调用父类的非静态初始化块进行初始化。这个调用是隐式执行的,而且父类的静态初始化块总是会被执行。接着调用父类一个或多个构造器执行初始化,这个调用既可以是通过super进行显式调用,也可以是隐式调用。

         当所有父类的非静态初始化块,构造器依次调用完成后,系统调用本类的非静态初始化块,构造器执行初始化,最后返回本类实例。

         如果有本图的继承关系:

bubuko.com,布布扣

程序会按如下步骤进行初始化:

(1)       执行Object类非静态初始化块(如果有的话)。

(2)       隐式或显式调用Object类的一个或多个构造器执行初始化。

(3)       执行Parent类非静态初始化块(如果有的话)。

(4)       隐式或显式调用Parent类的一个或多个构造器执行初始化。

(5)       执行Mid类非静态初始化块(如果有的话)。

(6)       隐式或显式调用Mid类的一个或多个构造器执行初始化。

(7)       执行Sub类非静态初始化块(如果有的话)。

(8)       隐式或显式调用Sub类的一个或多个构造器执行初始化。

 

只要在程序创建Java对象,系统总是先调用最顶层父类的初始化操作,包括初始化块和构造器,然后依次向下调用所有父类的初始化操作,最终执行本类的初始化操作返回本类的实例。至于调用父类的哪个构造器执行初始化,则分为如下几种情况:

(1)       子类构造器执行体的第一行代码使用super显式调用父类构造器,系统将根据super调用里传入的实参列表来确定调用父类的哪个构造器;

(2)       子类构造器执行的第一行代码使用this显式调用本类中重载的构造器,系统将根据this调用里传入的实参列表来确定本类的另一个构造器(执行本类中另一个构造器时即进入第一种情况);

(3)       子类构造器执行提中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。

(super调用用于显式调用父类的构造器,this调用用于显式调用本类中另一个重载的构造器。Super调用和this调用都只能在构造器中使用,而且super调用和this调用都必须作为构造器的第一行代码,因此构造器中的super调用和this调用最多只能使用其中之一,而且最多只能调用一次)。

 

2.2.2 访问子类对象的实例变量

         子类的方法可以访问父类的实例变量,这是因为子类继承父类就会获得父类的成员变量和方法;但父类的方法不能访问子类的实例变量,因为父类根本无从知道它将被哪个子类继承,它的子类将会增加怎样的成员变量。

         但是,在极端的情况下,可能出现父类访问子类变量的情况,例子如下:

class Base
{
    //定义了一个名为i的实例变量
    private int i = 2;
    public Base()
    {
        this.display();
    }
    public void display()
    {
        System.out.println(i);
    }
}
//继承Base的Derived子类
class Derived extends Base
{
    //定义了一个名为i的实例变量
    private int i = 22;
    //构造器,将实例变量i初始化为222
    public Derived()
    {
        i = 222;
    }
    public void display()
    {
        System.out.println(i);
    }
}
public class Test
{
    public static void main(String[] args)
    {
        //创建Derived的构造器创建实例
        new Derived();
    }
}

程序main里只有new Derived();。它会调用Derived的构造器,因为Derived继承了Dase,而且Derived构造器里没有显式使用super来调用父类的构造器,因此系统将会自动调用Base中无参数的构造器来初始化。

最后程序输出是0。不是22,也不是222,这是怎么回事呢?

为了解释这个程序,首先需要澄清一个概念:Java对象是由构造器创建的吗?很多书籍,资料中会说,是的。

但实际情况是:构造器只负责对Java对象实例变量执行初始化(也就是赋初始值),在执行构造器代码之前,该对象所占的内存已经被分配下来,这些内存里值都是默认是空值。

当程序执行new Derived();时,系统会先为Derived对象分配内存空间。此时系统内存需要为这个Derived对象分配两块内存,它们分别用于存放Derived对象的两个i实例变量,其中一个属于Base类定义的i,一个属于Derived类定义的i,此时这2个i的值都是0。

接下来程序在执行Derived的构造器之前,首先会执行Base类的构造器。表面上看,Base的构造器内只有一行代码this.display();,但由于Base类定义了i时指定了初始值是2,因此经过编译处理后,该构造器应该包含如下两行代码。

i = 2;

this.display();

         现在的问题就是此处的this代表谁?

         Java的定义是,当this在构造器中时,this代表正在初始化的Java对象。此时的情况是:从源代码来看,此时的this位于Base构造器内,但这些代码实际放在Derived构造器内执行,是Derived构造器隐式调用了Base构造器的代码。由此可见,此时的this应该是Derived对象,而不是Base对象。但是这个this虽然代表Derived对象,但它却位于Base构造器中,它的编译时类型是Base,而它实际引用了一个Derived对象。

         为了验证这一点,为Derived类增加一个简单的sub()方法,然后改造Base构造器

public Base()
    {
        System.out.println(this.i);
        this.display();
        System.out.println(this.getClass());
        //因为this的编译类型是Base,所以依然不能调用sub()方法
        this.sub();
    }

上面程序调用this.getClass()来获取this代表对象的类,将看到输出Derived类,这表明此时this引用代表是Derived对象,但程序调用sub()时,则无法通过编译,这是因为this的编译时类型是Base的缘故。

    当变量的编译时类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定。但通过该变量调用它引用的对象的实例方法时,该方法行为将由它实际所引用的对象来决定。因此,当程序访问this.i时,将会访问Base类中定义的i实例变量,也就是输出2;但执行this.display()时,则实际表现出Derived对象的行为,也就是输出Derived对象的i实例变量,即0。

 

2.2.3 调用被子类重写的方法

         在访问权限允许的情况下,子类可以调用父类方法,这是因为子类继承父类会获得父类定义的成员变量和方法;但父类不能调用子类的方法,因此父类根本无从知道它将被哪个子类继承,它的子类将会增加怎样的方法。

         但有一种特殊情况,当子类方法重写了父类方法之后,父类表面上只有调用属于自己的,被子类重写的方法,但随着执行context的改变,将会变成父类实际调用子类的方法。

         实例如下:

class Animal
{
    //desc实例变量保存对象toString方法的返回值
    private String desc;
    public Animal()
    {
        //调用getDesc()方法初始化desc实例变量
        this.desc = getDesc();
    }
    public String getDesc()
    {
        return "Animal";
    }
    public String toString()
    {
        return desc;
    }
}
public class Wolf extends Animal
{
    //定义name、weight两个实例变量
    private String name;
    private double weight;
    public Wolf(String name , double weight)
    {
        //为name、weight两个实例变量赋值
        this.name = name;
        this.weight = weight;
    }
    //重写父类的getDesc()方法
    @Override
    public String getDesc()
    {
        return "Wolf[name=" + name + " , weight="
            + weight + "]";
    }
    public static void main(String[] args)
    {
        System.out.println(new Wolf("灰太郎" , 32.3));
    }
}

这段代码会输出Wolf[name=null , weight=0.0],而不是Wolf[name=灰太狼 , weight=32.3],那我们赋的值哪去了?

    理解这个程序的关键在于this.desc = getDesc();。表面上此处是调用父类中定义的getDesc()方法,但实际运行过程中,此处会变成调用被子类重写的getDesc()方法。

    程序在执行new Wolf("灰太郎" , 32.3) 之前,系统会隐式执行父类无参数的构造器,也就是先执行this.desc = getDesc();但不再调用父类中定义的getDesc()方法,而是调用子类的getDesc()方法。此时还没有为name,weight赋值,所以会输出Wolf[name=null , weight=0.0]

    通过上面分析可以看到,该程序产生这种输出的原因在于this.desc = getDesc();处调用的getDesc()方法是被子类重写过的方法。这样使得对Wolf对象的实例变量赋值的语句this.name = name;this.weight = weight;在getDesc()方法之后被执行,因此getDesc()方法不能得到Wolf对象的name,weight实例变量的值。

    为了避免这种不希望看到的结果,应该避免在Animal类的构造器中调用被子类重写过的方法,因此将Animal类改成如下形式即可:

class Animal2
{
    public String getDesc()
    {
        return "Animal";
    }
    public String toString()
    {
        return getDesc();
    }
}

经过改写的Animal2类不再提供构造器(系统会为止提供一个无参数的构造器),程序改由toString()方法来调用被重写的getDesc()方法。这就保证了对Wolf对象的实例变量赋值的语句his.name = name;this.weight = weight;在getDesc()方法之前被执行,从而使得getDesc()方法得到Wolf对象的name,weight实例变量的值。

    如果父类构造器调用了被子类重写的方法,且通过子类构造器来创建子类对象,调用(不管是显式还是隐式)了这个父类构造器,就会导致子类的重写方法在子类构造器的所有代码之前被执行,从而导致子类的重写方法访问不到子类的实际变量值的情形。

 

2.3 父子实例的内存控制

继承是面向对象的3大特征之一,也是Java语言的重要特征,而父,子继承关系则是Java编程中需要重点注意的地方。下面将继续深入分析父子实例的内存控制。

2.3.1 继承成员变量和继承方法的区别

很多java书籍,资料都会介绍,当子类继承父类时,子类会获得父类中定义的成员变量和方法。当访问权限允许的情况下,子类可以直接访问父类中定义的成员变量和方法。这种介绍其实稍嫌笼统,因为Java继承中对成员变量和方法的处理是不同的,示例如下:

class Base
{
    int count = 2;
    public void display()
    {
        System.out.println(this.count);
    }
}
class Derived extends Base
{
    int count = 20;
    @Override
    public void display()
    {
        System.out.println(this.count);
    }
}
public class FieldAndMethodTest
{
    public static void main(String[] args)
    {
//      //声明、创建一个Base对象
        Base b = new Base();
//      //直接访问count实例变量和通过display访问count实例变量
        System.out.println(b.count);
        b.display();
        //声明、并创建一个Derived对象
        Derived d = new Derived();
        //直接访问count实例变量和通过display访问count实例变量
        System.out.println(d.count);
        d.display();
//      //声明一个Base变量,并将Derived对象赋给该变量
        Base bd = new Derived();    //(3)
//      //直接访问count实例变量和通过display访问count实例变量
        System.out.println(bd.count);
        bd.display();
//      //让d2b变量指向原d变量所指向的Dervied对象
        Base d2b = d;       //(4)
        //访问d2b所指对象的count实例变量
        System.out.println(d2b.count);
    }
}

在(3)这块代码中,直接通过db访问count实例变量,输出的将是Base(声明时的类型)对象的count实例变量的值;如果通过db来调用display()方法,该方法将表现出Derived(运行时类型)对象的行为方式。

    程序(4)代码直接将d变量赋值给d2b变量,只是d2b变量的类型是Base。这意味着d2b和d两个变量指向同一个Java对象,因此如果在程序中判断d2b==d,将返回true。但是访问d.count时输出20,访问d2b.count时却输出2。这一点看上去很诡异:两个指向同一个对象的变量,分别访问它们的实例变量时却输出不同的值。这表明在d2b,d变量所指向的Java对象中包含了两块内存,分别存放值为2的count实例变量和值为20的count实例变量。

    但不管是d变量,还是bd变量,d2b变量,只要它们实际指向一个Dervied对象,不管声明它们时用什么类型,当通过这些变量调用方法时,方法的行为总是表现出它们实际类型的行为;但如果通过这些变量来访问它们所指对象的实例变量,这些实例变量的值总是表现出声明这些变量所用类型的行为。由此可见,Java继承在处理成员变量和方法时是有区别的。

    如果在子类重写了父类方法,就意味着子类里定义了方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。对于实例变量则不存在这样的现象,即使子类中定义了与父类完全同名的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量。

    因为继承成员变量和继承方法之间存在这样的差别,所以对于一个引用类型的变量而言,当通过该变量访问它所引用的对象的实例变量时,该实例变量的值取决于声明该变量时类型;当通过该变量来调用它所引用的对象的方法时,该方法行为取决于它所实际引用的对象的类型。

2.3.2 内存中子类实例

看下面一段程序:

class Base
{
    int count = 2;
}
class Mid extends Base
{
    int count = 20;
}
public class Sub extends Mid
{
    int count = 200;
    public static void main(String[] args)
    {
        //创建一个Sub对象
        Sub s = new Sub();
        //将Sub对象向上转型后赋为Mid、Base类型的变量
        Mid s2m = s;
        Base s2b = s;
        //分别通过3个变量来访问count实例变量
        System.out.println(s.count);
        System.out.println(s2m.count);
        System.out.println(s2b.count);
    }
}

打印的结果是:

200

20

2

    s,s2m,s2b,这3个变量所引用Java对象拥有3个count实例变量,需要3块内存存储它们。如图:

bubuko.com,布布扣

从上图可以看出,Sub对象不仅存储了它自身的count实例变量,还存储从Mid,Base两个父类那里继承到的count实例变量。但这3个count实例变量在底层是有区别的,程序通过Base型变量来访问该对象的count实例变量时,输出2;通过Mid新的变量来访问该对象的count实例变量时,输出20;当直接在Sub类中访问实例变量i时,输出200。

         为了在Sub类中访问Mid类定义的count实例变量,可以在count之前加入super关键字为限定。例:super.count;。这样就可以访问到父类中定义的count实例变量。

         系统内存中并不存在Mid和Base两个对象,程序内存中只有一个Sub对象,只是这个Sub对象中不仅保存了在Sub类中定义的所有实例变量,还保存了它的所有父类所定义的全部实例变量。

         那super关键字的作用到底是什么?看下个程序:

class Fruit
{
    String color = "未确定颜色";
    //定义一个方法,该方法返回调用该方法的实例
    public Fruit getThis()
    {
        return this;
    }
    public void info()
    {
        System.out.println("Fruit方法");
    }
}
public class Apple extends Fruit
{
    //重写父类的方法
    @Override
    public void info()
    {
        System.out.println("Apple方法");
    }
    //通过super调用父类的Info()方法
    public void AccessSuperInfo()
    {
        super.info();
    }
    //尝试返回super关键字代表的内容
    public Fruit getSuper()
    {
        return super.getThis();
    }
    String color = "红色";
    public static void main(String[] args)
    {
        //创建一个Apple对象
        Apple a = new Apple();
        //调用getSuper()方法获取Apple对象关联的super引用
        Fruit f = a.getSuper();
        //判断a和f的关系
        System.out.println("a和f所引用的对象是否相同:" + (a == f));
        System.out.println("访问a所引用对象的color实例变量:" + a.color);
        System.out.println("访问f所引用对象的color实例变量:" + f.color);
        //分别通过a、f两个变量来调用info方法
        a.info();
        f.info();
        //调用AccessSuperInfo来调用父类的info()方法
        a.AccessSuperInfo();
    }
}

输出结果为:

a和f所引用的对象是否相同:true

访问a所引用对象的color实例变量:红色

访问f所引用对象的color实例变量:未确定颜色

Apple方法

Apple方法

Fruit方法

        

         Java程序允许某个方法通过return this;返回调用该方法的Java对象,但不允许直接return super;甚至不允许直接将super当成一个引用变量使用。

         通过上面程序可以看出:super关键字本身并没有引用任何对象,它甚至不能被当成一个真正的引用变量使用。主要有如下两个原因:

(1)       子类方法不能直接使用return super;但使用return this;返回调用该方法的对象是允许的。

(2)       程序不允许直接把super当成变量使用,例如,试图判断super和a变量是否引用同一个Java对象—super==a;但这条语句将引起编译错误。

至此,对父,子对象在内存中存储有了准确的结论:当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为其父类中定义的所有实例变量分配内存,即使子类定义了与父类中同名实例变量。

         如果在子类里定义了与父类中已有变量同名的变量,那么子类中定义的变量会隐藏父类中定义的变量。之一不是完全覆盖,因此系统为创建子类对象时,依然会为父类中定义的,被隐藏的变量分配内存空间。

         为了在子类方法中访问父类中定义的,被隐藏的实例变量,或者为了在子类方法中调用父类中定义的,被覆盖的方法,可以通过super作为限定来修饰这些实例变量和实例方法。

2.3.3 父,子类的类变量

         理解了上面介绍的父,子实例在内存中分配之后,接下来的父,子类的类变量基本与此类似。不同的是,类变量属于类本身,而实例变量则属于Java对象;类变量在类初始化阶段完成初始化,而实例变量则在对象初始化阶段完成初始化。

         由于类变量本质上属于类本身,因此通常不会涉及父,子实例变量那样复杂的情形,但由于Java允许通过对象来访问类变量,因此也可以使用super作为限定来访问父类中定义的类变量。下面程序示范了这种用法:

class StaticBase
{
    //定义一个count类变量
    static int count = 20;
}
public class StaticSub extends StaticBase
{
    //子类再定义一个count类变量
    static int count = 200;
    public void info()
    {
        System.out.println("访问本类的count类变量:" + count);
        System.out.println("访问父类的count类变量:" + StaticBase.count);
        System.out.println("访问父类的count类变量:" + super.count);
    }
    public static void main(String[] args)
    {
        StaticSub sb = new StaticSub();
        sb.info();
    }
}

要访问父类中定义的count类变量,有2中方式

(1)       直接使用父类的类名作为主调来访问count类变量。

(2)       使用super作为限定来访问count类变量。

 

2.4 final修饰符

         final修饰符是Java语言中比较简单的一个修饰符,定义如下:

(1)       final可以修饰变量,被final修饰的变量被赋初始值之后,不能对它重新赋值。

(2)       final可以修饰方法,被final修饰的方法不能被重写。

(3)       final可以修饰类,被final修饰的类不能派生子类。

只掌握定义是不够的,下面将从几个方面来分析final修饰符功能。

2.4.1 final修饰的变量

         被final修饰的实例变量必须显式指定初始值,而且只能在如下3个位置指定初始值。

(1)       定义final实例变量时指定初始值。

(2)       在非静态初始化块中为final实例变量指定初始值。

(3)       在构造器中为final实例变量指定初始值。

 

对于普通实例变量,Java程序可以对它执行默认的初始化,也就是将实例变量的值指定为默认的初始值0或null,但对于final实例变量,则必须由程序员显式指定初始值。

下面程序示范了在3个地方对final实例变量进行初始化。

public class FinalInstanceVaribaleTest
{
    //定义final实例变量时赋初始值
    final int var1 = "疯狂Java讲义".length();
    final int var2;
    final int var3;
    //在初始化块中为var2赋初始值
    {
        var2 = "轻量级Java EE企业应用实战".length();
    }
    //在构造器中为var3赋初始值
    public FinalInstanceVaribaleTest()
    {
        this.var3 = "疯狂XML讲义".length();
    }
    public static void main(String[] args)
    {
        FinalInstanceVaribaleTest fiv = new FinalInstanceVaribaleTest();
        System.out.println(fiv.var1);
        System.out.println(fiv.var2);
        System.out.println(fiv.var3);
    }
}

上面程序用了3种给final变量初始化的方式,但是经过编译器的处理,这3种方式都会被抽取到构造器中赋初始值。

对于final类变量而言,同样必须显式指定初始值,而且final类变量只能在2个地方初始值:

(1)       定义final类变量时指定初始值;

(2)       在静态初始化块中为final类变量指定初始值。

下面程序示范了2个地方对final类变量进行初始化

public class FinalClassVaribaleTest
{
    //定义final类变量时赋初始值
    final static int var1 = "疯狂Java讲义".length();
    final static int var2;
    //在静态初始化块中为var2赋初始值
    static {
        var2 = "轻量级Java EE企业应用实战".length();
    }
    public static void main(String[] args)
    {
        System.out.println(FinalClassVaribaleTest.var1);
        System.out.println(FinalClassVaribaleTest.var2);
    }
}

上面程序用了2种方法初始化,但是经过编译器的处理,这2种方式都会被抽取到静态初始化块中赋初始值。

当使用final修饰类变量时,如果定义该final类变量时指定了初始值,而且该初始值可以在编译时就被确定下来,系统将不会在静态初始化块中对该类变量赋初始值,而将是在类定义中直接使用该初始值代替该final变量。

对于一个使用final修饰的变量而言,如果定义该final变量时就指定初始值,而且这个初始值可以在编译时就确定下来,那么这个final变量将不再是一个变量,系统会将其当成“宏变量”处理。也就是说,所有出现该变量的地方,系统将直接把它当成对应的值处理。

 

2.4.2 执行“宏替换”的变量

对一个final变量,不管它是类变量,实例变量,还是局部变量,只要定义该变量时使用了final修饰符修饰,并在定义该final类变量时指定了初始值,而且该初始值可以在编译时就被确定下来,那么这个final变量本质上已经不再是变量,而是相当于一个直接量。

Final修饰符的一个重要用途就是定义“宏变量”。当定义final变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,那这个final变量本质上就是一个“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。

Java会缓存所有曾经用过的字符串直接量。例如执行String a = “java”;语句之后,系统的字符串池中就会缓存一个字符串”java”;如果程序再次执行String b = “java”;系统将会让b直接指向字符串池中的”java”字符串,因此a==b将会返回true。

 

为了加深对final修饰符的印象,看如下程序

public class StringJoinTest
{
    public static void main(String[] args)
    {
        String s1 = "疯狂Java";
        String s2 = "疯狂" + "Java";
        System.out.println(s1 == s2);
        //定义2个字符串直接量
        String str1 = "疯狂";
        String str2 = "Java";
        //将str1和str2进行连接运算
        String s3 = str1 + str2;
        System.out.println(s1 == s3);
    }
}

S1是一个普通的字符串直接赋值“疯狂Java”,s2的值是两个字符串直接进行连接运算,由于编译器可以在编译阶段就确定s2的值为“疯狂Java”,所以系统会让s2直接指向字符串池中缓存中的“疯狂Java”字符串,所以s1==s2输出true。

对于S3而言,它的值有str1和str2进行连接运算后得到。由于str1和str2只是两个普通变量,编译器不会执行“宏替换”,因此编译器无法在编译时确定s3的值,不会让s3指向字符串池中缓存中的“疯狂Java”。由此可见,s1==s3输出false。

为了让s1==s3输出true很简单,只要编译器可以对str1,str2两个变量执行“宏替换”。及在str1和str2之前加上fianl,这样编译器即可在编译阶段就确定s3的值,就会让s3指向字符串池中缓存中的“疯狂Java”。

对于实例变量而言,除了可以在定义该变量时赋初始值之外,还可以在非静态初始化块,构造器中对它赋初始值,而且在这3个地方指定初始值的效果基本一样。但对于fianl实例变量而言,只有在定义该变量时指定初始值才会有“宏变量”的效果,在非静态初始化块,构造器中为final实例变量指定初始值则不会有这种效果,示例如下:

public class FinalInitTest
{
    //定义3个final实例变量
    final String str1;
    final String str2;
    final String str3 = "Java";
    //str1、str2分别放在非静态初始化块、构造器中初始化
    {
        str1 = "Java";
    }
    public FinalInitTest()
    {
        str2 = "Java";
    }
    //判断str1、str2、str3是否执行"宏替换"
    public void display()
    {
        System.out.println(str1 + str1 == "JavaJava");
        System.out.println(str2 + str2 == "JavaJava");
        System.out.println(str3 + str3 == "JavaJava");
    }
    public static void main(String[] args)
    {
        FinalInitTest fit = new FinalInitTest();
        fit.display();
    }
}

上面程序str3是true,前两个都是false,就说明了这个问题。

 

2.4.3 final方法不能被重写

如果父类中某个方法使用了final修饰符进行修饰,那么这个方法将不可能被它的子类访问到,因此这个方法也不可能被它的子类重写。例子如下:

class Base
{
    private final void info()
    {
        System.out.println("Base的info方法");
    }
}
public class FinalMethodTest extends Base
{
    //这个info方法并不是覆盖父类方法。
//  @Override
    public void info()
    {
        System.out.println("FinalMethodTest的Info方法");
    }
}

子类FinalMethodTest的info方法只是一个普通方法,并不是重写父类的方法,如果打开@Override的话会提示错误。

 

2.4.4 内部类中的局部变量

         如果程序需要在匿名内部类中使用局部变量,那么这个局部变量必须使用final修饰符修饰。示例如下:

import java.util.*;
 
interface IntArrayProductor
{
    //接口里定义的product方法用于封装“处理行为”
    int product();
}
public class CommandTest
{
    //定义一个方法,该生成指定长度的数组,但每个数组元素由于cmd负责产生
    public int[] process(IntArrayProductor cmd  , int length)
    {
        int[] result = new int[length];
        for (int i = 0; i < length ; i++ )
        {
            result[i] = cmd.product();
        }
        return result;
    }
    public static void main(String[] args)
    {
        CommandTest ct = new CommandTest();
        final int seed = 5;
        //生成数组,具体生成方式取决于IntArrayProductor接口的匿名实现类
        int[] result = ct.process(new IntArrayProductor()
        {
            public int product()
            {
                return (int)Math.round(Math.random() * seed);
            }
        } , 6);
        System.out.println(Arrays.toString(result));
    }
}

不仅匿名内部类,即使是普通内部类,在任何内部类中访问的局部变量都应该使用final修饰。

那为什么Java要求内部类访问的局部变量必须使用final修饰呢?

Java要求所有被内部类访问的局部变量都使用final修饰也是有其原因的:对于普通局部变量而言,它的作用域就是停留在该方法内,当方法执行结束,该局部变量也随之消失;但内部类则可能产生隐式的“闭包”,闭包将使得局部变量脱离它所在的方法继续存在。

下面程序是局部变量脱离它所在方法继续存在的例子:

public class ClosureTest
{
    public static void main(String[] args)
    {
        //定义一个局部变量
        final String str = "Java";
        //在内部类里访问局部变量str
        new Thread(new Runnable()
        {
            public void run()
            {
                for (int i = 0; i < 100 ; i++ )
                {
                    //此处将一直可以访问到str局部变量
                    System.out.println(str + " " + i);
                    //暂停0.1秒
                    try
                    {
                        Thread.sleep(100);
                    }
                    catch (Exception ex)
                    {
                        ex.printStackTrace();
                    }
                }
            }
        }).start();
        //执行到此处,main方法结束
    }
}

上面程序定义了一个局部变量str。正常情况下,当程序执行完main后,他的声明周期就结束了,局部变量str的作用域也会随之结束。但只要新线程里的run方法没有执行完,匿名内部类的实例的生命周期就没有结束,将一直可以访问str局部变量的值,这就是内部类会扩大局部变量作用域的实例。

         由于内部类可能扩大局部变量的作用域,如果再加上这个被内部类访问的局部变量没有使用final修饰,也就是说该变量的值可以随意改变,那将引起极大的混乱,因此Java编译器要求所有被内部类访问的局部变量必须使用final修饰符修饰。

原文:http://www.cnblogs.com/zyaizz/p/3512131.html

评论(0
© 2014 bubuko.com 版权所有 - 联系我们:wmxa8@hotmail.com
打开技术之扣,分享程序人生!