对象与内存控制

对象与内存控制

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  构造器中对实例变量指定初始值。

其中第12种方式(定义时指定的初始值和非静态初始化块中指定的初始值)比第3种方式(构造器中指定初始值)更早执行,但第12种方式的执行顺序与它们在源程序中的排列顺序相同。

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次执行。

1double weight;创建Java对象时系统根据该语句为该对象分配内存。

2weight = 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参数代表)即得到该PricecurrentPrice变量值。

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)按初始化代码(定义时指定初始值和初始化块中执行初始值)的排列顺序对类变量执行初始化。

初始化第一阶段,系统先为INSTANCEinitPrice两个类变量分配内存空间,此时INSTANCEinitPrice的默认值为null0.0。接着初始化进入第二个阶段,程序按顺序依次为INSTANCEinitPrice进行赋值。对INSTANCE赋值时要调用Price2.8),创建Price实例,此时立即为currentPrice进行赋值,此时initPrice类变量的值为0,因此赋值结果是currentPrice等于-2.8。接着,程序再次将initPrice赋为20,但此时对INSTANCEcurrentPrice实例变量已经不起作用了。

Price类初始化完成后,INSTANCE类变量引用到一个currentPrice-2.8Price实例,而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的值,那么这个程序的将会输出多少呢?222222?。运行程序,会发现实际输出结果为0

从内存分配的角度来分析程序的输出结果,从而更好地把握程序运行的真实过程。

当程序创建Derived对象时,系统开始为这个Derived对象分配内存空间。需要指出的是,这个Derived对象并不是只有一个i实例变量,它将拥有两个i实例变量。

为了解释这个程序,首先需要澄清一个概念:Java对象是由构造器创建的吗?

实际情况是:构造器只是负责对Java对象实例变量执行初始化(也就是赋初始值),在执行构造器代码之前,该对象所占的内存已经被分配下来,这些内存里值都默认是空值——对于基本类型的变量,默认的空值就是0false;对于引用类型的变量,默认的空值就是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();

再次运行该程序,将看到输出20。看到这样的结果,可能会更加混乱了:些时的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的改变,将会变成父类实际调用子类的方法。

下面程序定义了两个具有父子关系的类AnimalWolf,其中Wolf重写了AnimalgetDesc()方法。

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构造器里的代码之前,系统会隐式执行父类无参数的构造器,此时Wolfnameweight实例变量将保持默认值——name的值为nullweight的值为0.0,因此WolfgetDesc()方法返回值是Wolf[name=null,weight=0.0],于是desc实例变量将被赋为Wolf[name=null,weight=0.0],这就是看到的输出结果。

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

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

class Animal2 {

  publicString getDesc() {

      return"Animal";

  }

 

  publicString toString() {

      returngetDesc();

   }

}

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

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

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()方法,就会导致编译器无法将Animalinfo()方法转移到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派生了MidMid派生了Sub,而且这3个类中都定义了名为count的实例变量。程序创建了一个Sub对象,并将这个Sub对象身上转型。

程序将会输出200202。这意味着submidbase3个变量所引用的Java对象拥有3count实例变量,也就是说需要3块内存存储它们。

这个Sub对象不仅存储了它自身的count实例变量,还需要存储从MidBase两个父类那里继承到的count实例变量。但这3count实例变量在底层是有区别的,程序通过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对象持有3count实例变量。

注意:系统内存中并不存在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当成变量使用,例如,试图判断supera变量是否引用同一个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修饰符

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

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

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

4.1 final修饰的变量

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

n  定义final实例变量时指定初始值;

n  在非静态初始化块中为final实例变量指定初始值;

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

对于普通实例变量,Java程序可以对它执行默认的初始化,也就是将实例变量的值指定为默认的初始值0null;但对于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修饰符进行修饰,那么这个方法将不可能被它的子类访问到,因此这个方法也不可能被它的子类重写。从这个意义上来说,privatefinal同时修饰某个方法没有太大意义,但是被Java语法允许的。

4.4 内部类中的局部变量

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值