对象与内存控制
Java内存管理分为两个方面:内存分配和内存回收。这里的内存分配特指创建Java对象时JVM为该对象在堆内存中所分配的内存空间。内存回收指的是当该Java对象失去引用,变成垃圾时,JVM的垃圾回收机制自动清理该对象,并回收该对象所占用的内存。由于JVM内置了垃圾回收机制回收失去引用的Java对象所占用的内存,所以很多Java开发者认为Java不存在内存泄漏、资源泄漏的问题。实际上这是一种错觉,Java程序依然会有内存泄漏。
由于JVM的垃圾回收机制由一条后台线程完成,本身也是非常消耗性能的,因此如果肆无忌惮地创建对象,让系统分配内存,那这些分配的内存都将由垃圾回收机制进行回收。这样做有两个坏处:
n 不断分配内存使得系统中可用内存减少,从而降低程序运行性能;
n 大量已分配内存的回收使得垃圾回收的负担加重,降低程序的运行性能。
1. 实例变量和类变量
Java程序的变量大体可分为成员变量和局部变量。其中局部变量可分为如下3类。
n 形参:在方法签名中定义的局部变量,由方法调用者负责为其赋值,随方法的结束而消亡。
n 方法内的局部变量:在方法内定义的局部变量,必须在方法内对其进行显式初始化。这种类型的局部变量从初始化完成后开始生效,随方法的结束而消亡。
n 代码块内的局部变量:在代码块内定义的局部变量,必须在代码块内对其进行显式初始化。这种类型的局部变量从初始化完成后开始生效,随代码块的结束而消亡。
局部变量的作用时间很短暂,它们都被存储在方法的栈内存中。
类体内定义的变量被称为成员变量。如果定义该成员变量时没有使用static修饰,该成员变量又被称为非静态变量或实例变量;如果使用了static修饰,则该成员变量又可被称为静态变量或类变量。
提示:static只能修饰在类里定义的成员部分,包括成员变量、方法、内部类、初始化块、内部枚举类。如果没有使用static修饰这些类里的成员,这些成员属于该类的实例;如果使用了static修饰,这些成员就属于类本身。static只能修饰类里的成员,不能修饰外部类,不能修饰局部变量、局部内部类。
表面上看,Java类里定义成员变量时没有先后顺序,但实际上Java要求定义成员变量时必须采用合法的前向引用。
public class ErrorDef {
// 下面代码将提示:非法向前引用
int num1= num2 + 2;
int num2=20;
}
上面程序中定义num1成员变量的初始值时,需要根据num2变量的值进行计算,这就是“非法前向引用”。因此,编译上面程序将提示“非法前向引用”的错误。
类似地,两个类变量也不允许采用这种“非法前向引用”。
public class ErrorDef {
// 下面代码将提示:非法前向引用
staticint num1 = num2 + 2;
staticint num2 = 20;
}
但如果一个是实例变量,一个是类变量,则实例变量总是可以引用类变量。
public class RightDef {
// 下面代码将完全正常
int num1= num2 + 2;
staticint num2= 20;
}
1.1 实例变量和类变量的属性
使用static修饰的成员变量是类变量,属于该类本身;没有使用static修饰的成员变量是实例变量,属于该类的实例。在同一个JVM内,每个类只对应一个Class对象,但每个类可以创建多个Java对象。
由于同一个JVM内每个类只对应一个Class对象,因此同一个JVM内的一个类的类变量只需一块内存空间;但对于实例变量而言,该类每创建一次实例,就需要为实例变量分配一块内存空间。也就是说,程序中有几个实例,实例变量就需要几块内存空间。
publicclassPerson {
privateString name;
privateintage;
staticinteyeNum = 2;
publicPerson(String name, int age) {
this.name = name;
this.age = age;
}
publicString getName() {
returnname;
}
publicvoid setName(String name) {
this.name = name;
}
publicint getAge() {
returnage;
}
publicvoid setAge(int age) {
this.age = age;
}
}
public class FieldTest {
publicstatic void main(String[] args) {
Personp1 = new Person("rain", 25);
Personp2 = new Person("rainbow", 26);
//通过p1、p2访问类变量
System.out.println(p1.eyeNum);
System.out.println(p2.eyeNum);
//通过Person访问类变量
System.out.println(Person.eyeNum);
//修改类变量
Person.eyeNum = 3;
//通过p1、p2访问类变量
System.out.println(p1.eyeNum);
System.out.println(p2.eyeNum);
//通过Person访问类变量
System.out.println(Person.eyeNum);
}
}
注意:大部分时候会把类和对象严格地区分开,但从另一个角度来看,类也是对象,所有类都是Class的实例。每个类初始化完成之后,系统都会为该类创建一个对应的Class实例,程序可以通过反射来获取某个类所对应的Class实例。例如,要获取Person类对应的Class实例,通过Person.class;或Class.forName("Person");任意一条代码即可。
1.2 实例变量的初始化时机
对于实例变量而言,它属于Java对象本身,每次程序创建Java对象时都需要为实例变量分配内存空间,并执行初始化。
从程序运行的角度来看,每次创建Java对象都会为实例变量分配内存空间,并对实例变量执行初始化。
从语法角度来看,程序可以在3个地方对实例变量进行初始化:
n 定义实例变量时指定初始值;
n 非静态初始化块中对实例变量指定初始值;
n 构造器中对实例变量指定初始值。
其中第1、2种方式(定义时指定的初始值和非静态初始化块中指定的初始值)比第3种方式(构造器中指定初始值)更早执行,但第1、2种方式的执行顺序与它们在源程序中的排列顺序相同。
public class Cat {
privateString name;
privateint age;
{
System.out.println("执行非静态初始化");
weight= 2.3;
}
privatedouble weight = 2.0;
publicCat(String name, int age) {
System.out.println("执行构造器");
this.name= name;
this.age= age;
}
publicString getName() {
returnname;
}
publicvoid setName(String name) {
this.name= name;
}
publicint getAge() {
returnage;
}
publicvoid setAge(int age) {
this.age= age;
}
publicdouble getWeight() {
returnweight;
}
publicvoid setWeight(double weight) {
this.weight= weight;
}
@Override
publicString toString() {
returnthis.name + "\t" + this.age + "\t" + this.weight;
}
}
public class InitTest {
publicstatic void main(String[] args) {
Catcat1 = new Cat("rain", 2);
System.out.println(cat1);
Catcat2 = new Cat("rainbow", 3);
System.out.println(cat2);
}
}
运行结果:
执行非静态初始化
执行构造器
rain 2 2.0
执行非静态初始化
执行构造器
rainbow 3 2.0
每当程序调用指定构造器来创建Java对象时,该构造器必然会获得执行的机会。除此之外,该类所包含的非静态初始化块将会获得执行的机会,而且总是在构造器执行之前获得执行。即程序将会先执行非静态初始化块,再调用构造器来初始化实例。
定义实例变量时指定的初始值、初始化块中为实例变量指定初始值的语句的地位是平等的,当经过编译器处理后,它们都将被提取到构造器中。也就是说,对于类定义中的语句:
double weight = 2.3;
实际上会被分成如下2次执行。
(1)double weight;创建Java对象时系统根据该语句为该对象分配内存。
(2)weight = 2.3;这条语句将会被提取到Java类的构造器中执行。
1.3 类变量的初始化时机
类变量属于Java类本身,只有当程序初始化该Java类时才会为该类的类变量分配内存空间,并执行初始化。
从程序运行的角度来看,JVM对一个Java类只初始化一次,因此Java程序每运行一次,系统只为类变量分配一次内存空间,执行一次初始化。
从语法角度来看,程序可以在2个地方对类变量执行初始化:
n 定义类变量时指定初始值;
n 静态初始化块中对类变量指定初始值。
这两种方式的执行顺序与它们在源程序中排列顺序相同。
public class StaticInitTest {
static {
name= "rainbow";
}
privatestatic String name = "rain";
publicstatic void main(String[] args) {
System.out.println(StaticInitTest.name);
}
}
运行结果:
rain
下面程序清楚地表现了类变量的初始化过程。首先定义了Price类,该Price类里有一个静态的initPrice变量,用于代表初始价格。每次创建Price实例时,系统会以initPrice为基础,减去当前打折价格(由discount参数代表)即得到该Price的currentPrice变量值。
public class Price {
staticfinal Price INSTANCE = new Price(2.8);
staticdouble initPrice = 20;
doublecurrentPrice;
publicPrice(double discount) {
currentPrice= initPrice - discount;
}
}
public class PriceTest {
publicstatic void main(String[] args) {
System.out.println(Price.INSTANCE.currentPrice);
Priceprice = new Price(2.8);
System.out.println(price.currentPrice);
}
}
运行结果:
-2.8
17.2
第一次用到Price类时,程序开始对Price类进行初始化,初始化分成以下2个阶段。
(1)系统为Price的两个类变量分配内存空间。
(2)按初始化代码(定义时指定初始值和初始化块中执行初始值)的排列顺序对类变量执行初始化。
初始化第一阶段,系统先为INSTANCE、initPrice两个类变量分配内存空间,此时INSTANCE、initPrice的默认值为null和0.0。接着初始化进入第二个阶段,程序按顺序依次为INSTANCE、initPrice进行赋值。对INSTANCE赋值时要调用Price(2.8),创建Price实例,此时立即为currentPrice进行赋值,此时initPrice类变量的值为0,因此赋值结果是currentPrice等于-2.8。接着,程序再次将initPrice赋为20,但此时对INSTANCE的currentPrice实例变量已经不起作用了。
当Price类初始化完成后,INSTANCE类变量引用到一个currentPrice为-2.8的Price实例,而initPrice类变量的值为20.0。当再次创建Price实例时,该Price实例的currentPrice实例变量的值才等于20.0-discount。
2 父类构造器
当创建任何Java对象时,程序总会先依次调用每个父类非静态初始化块、父类构造器(总是从Object开始)执行初始化,最后才调用本类的非静态初始化块、构造器执行初始化。
2.1 隐式调用和显式调用
当调用某个类的构造器来创建Java对象时,系统总会先调用父类的非静态初始化块进行初始化。这个调用是隐式执行的,而且父类的静态初始化块总是会被执行的。接着会调用父类的一个或多个构造器执行初始化,这个调用既可以是通过super进行显式调用,也可以是隐式调用。
当所有父类的非静态初始化块、构造器依次调用完成后,系统调用本类的非静态初始化块、构造器执行初始化,最后返回本类的实例。
public class Creature {
{
System.out.println("Creature非静态初始化块执行");
}
publicCreature(){
System.out.println("Creature构造方法执行");
}
}
public class Animal extends Creature {
{
System.out.println("Animal非静态初始化块执行");
}
publicAnimal() {
System.out.println("Animal构造方法执行");
}
}
public class Wolf extends Animal {
{
System.out.println("Wolf非静态初始化块执行");
}
publicWolf() {
System.out.println("Wolf构造方法执行");
}
}
public class InitTest {
publicstatic void main(String[] args) {
new Wolf();
}
}
运行结果:
Creature非静态初始化块执行
Creature构造方法执行
Animal非静态初始化块执行
Animal构造方法执行
Wolf非静态初始化块执行
Wolf构造方法执行
只要程序创建Java对象,系统总是先调用最顶层父类的初始化操作,包括初始化块和构造器,然后依次向下调用所有父类的初始化操作,最终执行本类的初始化操作返回本类的实例。至于调用父类的哪个构造器执行初始化,则分为如下几种情况:
n 子类构造器执行体的第一行代码使用super显式调用父类构造器,系统将根据super调用里传入的实例列表来确定调用父类的哪个构造器;
n 子类构造器执行体的第一行代码使用this显式调用本类中重载的构造器,系统将根据this调用里传入的实参列表来确定本类的另一个构造器(最终会进入第一种情况);
n 子类构造器执行体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。
注意:super调用用于显式调用父类的构造器,this调用用于显式调用本类中另一个重载的构造器。super调用和this调用都只能在构造器中使用,而且super调用和this调用都必须作为构造器的第一行代码,因此构造器中的super调用和this调用最多只能使用其中之一,而且最多只能调用一次。
2.2 访问子类对象的实例变量
子类的方法可以访问父类的实例变量,这是因为子类继承父类就会获得父类的成员变量和方法;但父类的方法不能访问子类的实例变量,因为父类根本无从知道它将被哪个子类继承,它的子类将会增加怎样的成员变量。
但是,在极端的情况下,可能出现父类访问子类变量的情况。
public class Base {
privateint i = 2;
publicBase() {
this.display();
}
publicvoid display() {
System.out.println(i);
}
}
public class Derived extends Base {
privateint i = 22;
publicDerived() {
i =222;
}
publicvoid display() {
System.out.println(i);
}
}
public class Tester {
publicstatic void main(String[] args) {
newDerived().display();
}
}
上面程序的main方法里只有一行代码:new Derived();。这行代码将会调用Derived里的构造器,Derived类继承了Base类,而且Derived构造器里没有显式使用super来调用父类的构造器,因此系统将会自动调用Base类中无参数的构造器来执行初始化。
在Base类的无参数的构造器,只是简单地调用了this.display()方法来输出实例变量i的值,那么这个程序的将会输出多少呢?2?22?222?。运行程序,会发现实际输出结果为0。
从内存分配的角度来分析程序的输出结果,从而更好地把握程序运行的真实过程。
当程序创建Derived对象时,系统开始为这个Derived对象分配内存空间。需要指出的是,这个Derived对象并不是只有一个i实例变量,它将拥有两个i实例变量。
为了解释这个程序,首先需要澄清一个概念:Java对象是由构造器创建的吗?
实际情况是:构造器只是负责对Java对象实例变量执行初始化(也就是赋初始值),在执行构造器代码之前,该对象所占的内存已经被分配下来,这些内存里值都默认是空值——对于基本类型的变量,默认的空值就是0或false;对于引用类型的变量,默认的空值就是null。
当程序创建Derived对象时,系统会先为Derived对象分配内存空间。此时系统内存需要为这个Derived对象分配两块内存,它们分别用于存放Derived对象的两个i实例变量,其中一个属于Base定义的i实例变量,一个属于Derived类定义的i实例变量,此时这两个i实例变量的值都是0。
接下来程序在执行Derived类的构造器代码之前,首先会执行Base类的构造器。表面上看,Base类的构造器内只有一行代码this.display();但由于Base类定义i实例变量时指定了初始值2,因此经过编译器处理后,该构造器应该包含如下两行代码。
i = 2;
this.display();
因此,程序先将Base类中定义的i实例变量赋值为2,再调用this.display()方法。此处this代表谁?
回答这个总是之前,先进行一些简单的修改,将Based类的构造器改为如下形式。
public Base() {
System.out.println(this.i);
this.display();
}
现在,Base构造器里表面上只有2行代码,实际上应该有3行代码,如下所示。
i=2;
System.out.println(this.i);
this.display();
再次运行该程序,将看到输出2、0。看到这样的结果,可能会更加混乱了:些时的this到底代表谁?
当this在构造器中时,this代表正在初始化的Java对象。此时的情况是:从源代码来看,此时的this位于Base()构造器内,但这些代码实际放在Derived()构造器内执行——是Derived构造器隐式调用了Base()构造器的代码。由此可见,此时的this应该是Derived对象,而不是Base对象。
现在总是又出现了,既然this引用代表了Derived对象,那怎么直接输出this.i时会输出2呢?这是因为,这个this虽然代表Derived对象,但它却位于Base构造器中,它的编译时类型是Base,而它实际引用一个Derived对象。
为Derived类增加一个简单的sub()方法,然后将Base构造器改为如下形式。
public Base() {
System.out.println(this.i);
this.display();
System.out.println(this.getClass());
//this.sub();
}
上面程序调用this.getClass()来获取this代表对象的类,将看到输出Derived类,这表明此时this引用的是Derived对象。但接下来,程序通过this调用sub()方法时,则无法通过编译,这就是因为this的编译时类型是Base的缘故。
当变量的编译时类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定。但通过该变量调用它引用的对象的实例方法时,该方法行为将由它实际所引用的对象来决定。因此,当程序访问this.i时,将会访问Base类中定义的i实例变量,也就是将输出2;但执行this.display();代码时,则实际表现出Derived对象的行为,也就是输出Derived对象的i实例变量,即0。
2.3 调用被子类重写的方法
在访问权限允许的情况下,子类可以调用父类方法,这是因为子类继承父类会获得父类定义的成员变量和方法;但父类不能调用子类的方法,因为父类根本无从知道它将被哪个子类继承,它的子类将会增加怎样的方法。
但有一种特殊的情况,当子类方法重写了父类方法之后,父类表面上只是调用属于自己的、被子类重写的方法,但随着执行context的改变,将会变成父类实际调用子类的方法。
下面程序定义了两个具有父子关系的类Animal和Wolf,其中Wolf重写了Animal的getDesc()方法。
public class Animal {
privateString desc;
publicAnimal() {
this.desc= getDesc();
}
publicString getDesc() {
return"Animal";
}
@Override
publicString toString() {
returndesc;
}
}
public class Wolf extends Animal {
privateString name;
privatedouble weight;
publicWolf(String name, double weight) {
this.name= name;
this.weight= weight;
}
@Override
publicString getDesc() {
return"Wolf[name" + name + ",weight=" + weight + "]";
}
}
public class Tester {
publicstatic void main(String[] args) {
System.out.println(newWolf("rainbow", 1100));
}
}
运行结果:
Wolf[name=null,weight=0.0]
表面上此处是调用父类中定义的getDesc()方法,但实际运行过程中,此处会变为调用被子类重写的getDesc()方法。
在执行Wolf构造器里的代码之前,系统会隐式执行父类无参数的构造器,此时Wolf的name、weight实例变量将保持默认值——name的值为null,weight的值为0.0,因此Wolf的getDesc()方法返回值是Wolf[name=null,weight=0.0],于是desc实例变量将被赋为Wolf[name=null,weight=0.0],这就是看到的输出结果。
通过上面分析可以看到,该程序产生这种输出的原因在于,调用的getDesc()方法是被子类重写过的方法。这样使得对Wolf对象的实例变量赋值的语句this.name =name;this.weight = weight;在getDesc()方法之后被执行,因此getDesc()方法不能得到Wolf对象的name、weight实例变量的值。
为了避免这种不希望看到的结果,应该避免在Animal类的构造器中调用被子类重写过的方法,因此将Animal类改为如下形式即可。
class Animal2 {
publicString getDesc() {
return"Animal";
}
publicString toString() {
returngetDesc();
}
}
经过改写的Animal2类不再提供构造器(系统会为之提供一个无参数的构造器),程序改由toString()方法来调用被重写的getDesc()方法。这就保证了对Wolf对象的实例变量赋值的语句this.name = name;this.weight = weight;在getDesc()方法之前被执行,从而使得getDesc()方法得到Wolf对象的name、weight实例变量的值。
注意:如果父类构造器调用了被子类重写的方法,且通过子类构造器来创建子类对象,调用(不管是显式还是隐式)了这个父类构造器,就会导致子类的重写方法在子类构造器的所有代码之前被执行,从而导致子类的重写方法访问不到子类的实例变量值的情形。
3. 父子实例的内存控制
继承是面向对象的3大特征之一,也是Java语言的重要特性,而父、子继承关系则是Java编程中需要重点注意的地方。
3.1 继承成员变量和继承方法的区别
很多资料都会介绍:当子类继承父类时,子类会获得父类中定义的成员变量和方法。当访问权限允许的情况下,子类可以直接访问父类中定义的成员变量和方法。这种介绍其实稍嫌笼统,因为Java继承中对成员变量和方法的处理是不同的。
public class Base {
int count= 2;
public voiddisplay() {
System.out.println(this.count);
}
}
public class Derived extends Base {
int count= 22;
@Override
publicvoid display() {
System.out.println(this.count);
}
}
public class FieldAndMethodTester {
publicstatic void main(String[] args) {
Basebase = new Base();
System.out.println("base.count:"+ base.count);
System.out.print("base.display():");
base.display();
Derivedderived = new Derived();
System.out.println("derived.count:"+ derived.count);
System.out.print("derived.display():");
derived.display();
Basebd = new Derived();
System.out.println("db.count:"+bd.count);
System.out.print("db.display():");
bd.display();
}
}
不管声明变量时用什么类型,当通过这些变量调用方法时,方法的行为总是表现出它们实际类型的行为;但如果通过这些变量来访问它们所指对象的实例变量,这些实例变量的值总是表现出声明这些变量所用类型的行为。
public class Animal {
privateString name;
publicvoid info() {
System.out.println(name);
}
}
public class Wolf extends Animal {
privatedouble weight;
}
当Wolf类继承Animal类时,编译器会直接将Animal里的info()方法转移到Wolf类中。这意味着,如果Wolf类也包含了info()方法,就会导致编译器无法将Animal的info()方法转移到Wolf类中。
对于Animal中定义的成员变量name而言,系统依然将其保留在Animal类中,并不会将它转移到其子类Wolf类中。这使得Animal类和Wolf类可以同时拥有同名的实例变量。
如果在子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。对于实例变量则不存在这样的现象,即便子类中定义了与父类完全同名的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量。
因为继承成员变量和继承方法之间存在这样的差别,所以对于一个引用类型的变量而言,当通过该变量访问它所引用的对象的实例变量时,该实例变量的值取决于声明该变量时的类型;当通过该变量来调用它所引用的对象的方法时,该方法行为取决于它所实际引用的的对象的类型。
3.2 内存中子类实例
public class Base {
int count= 2;
}
public class Mid extends Base {
int count= 20;
}
public class Sub extends Mid {
int count= 200;
}
public class Tester {
publicstatic void main(String[] args) {
Subsub = new Sub();
Midmid = sub;
Basebase = sub;
System.out.println("Sub.count:"+ sub.count);
System.out.println("Mid.count:"+ mid.count);
System.out.println("Base.count:"+ base.count);
}
}
上面程序中定义了3个带有父子关系的类:Base派生了Mid,Mid派生了Sub,而且这3个类中都定义了名为count的实例变量。程序创建了一个Sub对象,并将这个Sub对象身上转型。
程序将会输出200、20、2。这意味着sub、mid、base这3个变量所引用的Java对象拥有3个count实例变量,也就是说需要3块内存存储它们。
这个Sub对象不仅存储了它自身的count实例变量,还需要存储从Mid、Base两个父类那里继承到的count实例变量。但这3个count实例变量在底层是有区别的,程序通过Base型变量来访问该对象的count实例变量时,将输出2;通过Mid型的变量来访问该对象的count实例变量时,将输出20;当直接在Sub类中访问实例变量i时,程序输出200,即访问到Sub类中定义的实例变量i。
为了在Sub类访问Mid类定义的count实例变量,可以在count实例变量之前增加super关键字作为限定。例如,在Sub类增加如下方法。
public void accessMid() {
System.out.println(super.count);
}
上面的accessMid()方法就可以访问到父类中定义的count实例变量。那么此处的super代表什么?很多人说:super代表父类的默认实例,这个说法含糊而笼统,如果super代表父类的默认实例,那么这个默认实例在哪里?
实际上会发现系统中只有一个Sub对象,而且这个Sub对象持有3个count实例变量。
注意:系统内存中并不存在Mid对象和Base两个对象,程序内存中只有一个Sub对象,只是这个Sub对象中不仅保存了在Sub类中定义的所有实例变量,还保存了它的所有父类所定义的全部实例变量。
public class Fruit {
Stringcolor = "未确定的颜色";
publicFruit getThis() {
returnthis;
}
publicvoid info() {
System.out.println("Fruitinfo()");
}
}
public class Apple extends Fruit {
Stringcolor = "红色";
@Override
publicvoid info() {
System.out.println("Appleinfo()");
}
publicFruit getSupper() {
returnsuper.getThis();
}
publicvoid accessSuperInfo() {
super.info();
}
}
public class Tester {
publicstatic void main(String[] args) {
Appleapple = new Apple();
Fruitfruit = apple.getSupper();
System.out.println("apple与fruit引用的对象是否相同:" + (apple == fruit));
System.out.println("访问apple所引用对象的实例变量color:" + apple.color);
System.out.println("访问fruit所引用对象的实例变量color:" + fruit.color);
apple.info();
fruit.info();
apple.accessSuperInfo();
}
}
运行结果:
apple与fruit引用的对象是否相同:true
访问apple所引用对象的实例变量color:红色
访问fruit所引用对象的实例变量color:未确定的颜色
Apple info()
Apple info()
Fruit info()
通过Apple对象的getSupper()方法所返回的实际上是该Apple对象本身,只是它的声明类型是Fruit,因此通过fruit变量访问color实例变量时,该实例变量的值由Fruit类决定;但通过fruit变量调用info()方法时,该方法的行为由fruit变量实际所引用的Java对象决定,因此程序输出“Apple info()”。
当程序在Apple类的accessSuperInfo()方法使用super作为限定调用info()方法,该info()才真正表现出Fruit类的行为。
通过上面的分析可以看出:super关键字本身并没有引用任何对象,它甚至不能被当成一个真正的引用变量使用。主要有如下两个原因:
n 子类方法不能直接使用return super;,但使用returnthis;返回调用该方法的对象是可以的;
n 程序不允许直接把super当成变量使用,例如,试图判断super和a变量是否引用同一个Java对象——super==a;这条语句将引起编译错误。
结论:当程序创建一个子类对象时,系统不仅为该类中定义的实例变量分配内存,也会为其父类中定义的所有实例变量分配内存,即便子类定义了与父类中同名实例变量。
如果在子类里定义了与父类中已有变量同名的变量,那么子类中定义的变量会隐藏父类中定义的变量。注意不是完全覆盖,因此系统在创建子类对象时,依然会为父类中定义的、被隐藏的变量分配内存空间。
为了在子类方法中访问父类中定义的、被隐藏的实例变量,或者为了在子类方法中调用父类中定义的、被重写(Override)的方法,可以通过super作为限定符来修饰这些实例变量和实例方法。
3.3 父、子类的类变量
类变量属于类本身,而实例变量则属于Java对象;类变量在类初始化阶段完成初始化,而实例变量则在对象初始化阶段完成初始化。
由于类变量本质上属于类本身,因此通过学会涉及父、子实例变量那样复杂的情形,但由于Java允许通过对象来访问类变量,因此也可以使用super作为限定来访问父类中定义的类变量。
public class Base {
staticint count = 3;
}
public class Sub extends Base {
}
public class Tester {
publicstatic void main(String[] args) {
System.out.println("Base.count:"+ Base.count);
System.out.println("Sub.count:"+ Sub.count);
Sub.count= 30;
System.out.println("Base.count:"+ Base.count);
System.out.println("Sub.count:"+ Sub.count);
}
}
运行结果:
Base.count:3
Sub.count:3
Base.count:30
Sub.count:30
父、子类中类变量重名的情况如下所示。
public class Base {
staticint count = 3;
}
public class Sub extends Base {
staticint count = 30;
}
public class Tester {
publicstatic void main(String[] args) {
System.out.println("Base.count:"+ Base.count);
System.out.println("Sub.count:"+ Sub.count);
Base.count= 300;
Sub.count= 3000;
System.out.println("Base.count:"+ Base.count);
System.out.println("Sub.count:"+ Sub.count);
}
}
运行结果:
Base.count:3
Sub.count:30
Base.count:300
Sub.count:3000
如果需要访问父类中定义的count类变量,程序有两种方式:
n 直接使用父类的类名作为主调来访问count类变量;
n 使用super作为限定来访问count类变量。
publicclass Base{
staticintcount = 3;
}
public class Sub extends Base {
staticint count = 30;
publicvoid info(){
System.out.println("访问Sub类的类变量count:"+ count);
System.out.println("访问Based类的类变量count:"+ Base.count);
System.out.println("访问Based类的类变量count:"+ super.count);
}
}
public class Tester {
publicstatic void main(String[] args) {
Subsub = new Sub();
sub.info();
}
}
通常建议采用使用类名来访问类变量,因为类变量属于类本身,总是使用类名作为主调来访问类变量,能保持最好的代码可读性。
4. final修饰符
n final可以修饰变量,被final修饰的变量被赋初始值之后,不能对它重新赋值。
n final可以修饰方法,被final修饰的方法不能被重写。
n final可以修饰类,被final修饰的类不能派生子类。
4.1 final修饰的变量
final修饰的实例变量必须显式指定初始值,而且只能在如下3个位置指定初始值。
n 定义final实例变量时指定初始值;
n 在非静态初始化块中为final实例变量指定初始值;
n 在构造器中为final实例变量指定初始值。
对于普通实例变量,Java程序可以对它执行默认的初始化,也就是将实例变量的值指定为默认的初始值0或null;但对于final实例变量,则必须由程序员显式指定初始值。
public class FinalVarialble {
final intvar1 = 1;
final intvar2;
final intvar3;
{
var2= 2;
}
publicFinalVarialble() {
var3= 3;
}
publicstatic void main(String[] args) {
FinalVarialblefinalVarialble = new FinalVarialble();
System.out.println(finalVarialble.var1);
System.out.println(finalVarialble.var2);
System.out.println(finalVarialble.var3);
}
}
需要指出的是,经过编译器的处理,这3种方式都会被抽取到构造器中赋初始值。
对于final类变量而言,同样必须显式指定初始值,而且final类变量只能在2个地方指定初始值:
n 定义final类变量时指定初始值;
n 在静态初始化块中为final类变量指定初始值。
public class FinalClassVarialble {
staticfinal int var1 = 1;
staticfinal int var2;
static {
var2 = 2;
}
publicstatic void main(String[] args) {
System.out.println(FinalClassVarialble.var1);
System.out.println(FinalClassVarialble.var2);
}
}
需要指出的是,经过编译器的处理,这2种方式都会被抽取到静态初始化中赋初始值。
final修饰局部变量的情形比较简单——Java本来就要求局部变量必须被显式地赋初始值,final修饰的局部变量一样需要被显式的赋初始值(编程时并不要求定义变量时就赋值)。与普通变量不同的是:final修饰的局部变量被赋初始值之后,以后再也不能对final局部变量重新赋值。
public class Price {
staticfinal Price INSTANCE = new Price(2.8);
staticdouble initPrice = 20.0;
doublecurrentPrice;
publicPrice(double discount){
currentPrice = initPrice - discount;
}
publicstatic void main(String[] args){
System.out.println(Price.INSTANCE.currentPrice);
Priceprice = new Price(2.8);
System.out.println(price.currentPrice);
}
}
运行结果:
-2.8
17.2
修改代码:
public class Price {
staticfinal Price INSTANCE = new Price(2.8);
staticfinal double initPrice = 20.0;
doublecurrentPrice;
publicPrice(double discount){
currentPrice= initPrice - discount;
}
publicstatic void main(String[] args){
System.out.println(Price.INSTANCE.currentPrice);
Priceprice = new Price(2.8);
System.out.println(price.currentPrice);
}
}
运行结果:
17.2
17.2
对比上面两个输出结果:不难发现当使用final修饰类变量时,如果定义该final类变量时指定了初始值,而且该初始值可以在编译时就被确定下来,系统将不会在静态初始化块中对该类变量赋初始值,而将是在类定义中直接使用该民初始值代替该final变量。
对于一个使用final修饰的变量而言,如果定义该final变量时就指定初始值,而且这个初始值可以在编译时就确定下来,那么这个final变量将不再是一个变量,系统会将其当成“宏变量”处理。也就是说,所有出现该变量的地方,系统将直接把它当成对应的值处理。
4.2 执行“宏替换”的变量
对一个final变量,不管它是类变量、实例变量,还是局部变量,只要定义该变量时使用了final修饰符修饰,并在定义该final类变量时指定了初始值,而且该初始值可以在编译时就被确定下来,那么这个final变量本质上已经不再是变量,而是相当于一个直接量。
除了那种为final变量赋值时赋直接量的情况外,如果被赋的表达式只是基本的算术运算表达式或字符串连接运算,没有访问普通变量,调用方法,Java编译器同样会将这种final变量当成“宏变量”处理。
对于实例变量而言,除了可以在定义该变量时赋初始值之外,还可以在非静态初始块、构造器中对它赋初始值,而且这3个地方指定初始值的效果基本一样。但对于final实例变量而言,只有在定义该变量时指定初始值才会有“宏变量”的效果,在非静态初始化块、构造器中为final实例变量指定初始值则不会有这种效果。
同样,对于final类变量而言,只有在定义final类变量时指定初始值,系统才会对该final类变量执行“宏替换”。
4.3 final方法不能被重写
当final修饰某个方法时,用于限制该方法不可被它的子类重写。
如果父类中某个方法使用了private修饰符进行修饰,那么这个方法将不可能被它的子类访问到,因此这个方法也不可能被它的子类重写。从这个意义上来说,private和final同时修饰某个方法没有太大意义,但是被Java语法允许的。
4.4 内部类中的局部变量
如果程序需要在匿名内部类中使用局部变量,那么这个局部变量必须使用final修饰符修饰。