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

      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);
    	}
    }
    

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

    当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实例,内存分配如图:

    从上图可以看出,该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进行显式调用,也可以是隐式调用。

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

             如果有本图的继承关系:

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

    (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块内存存储它们。如图:

    从上图可以看出,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修饰符修饰。

  • 相关阅读:
    jQuery 基本选择器
    JavaScriptif while for switch流程控制 JS函数 内置对象
    JavaScrip基本语法
    数据库 存储引擎 表的操作 数值类型 时间类型 字符串类型 枚举集合 约束
    数据库基础知识 管理员 用户登录授权的操作
    粘包的产生原理 以及如何解决粘包问题
    socket TCP DPT 网络编程
    2018年年终总结
    Android技术分享
    No accelerator found
  • 原文地址:https://www.cnblogs.com/zyaizz/p/3512131.html
Copyright © 2011-2022 走看看