ITPub博客

首页 > Linux操作系统 > Linux操作系统 > Java私塾跟我学系列——JAVA篇 第五章 Java高级类特性

Java私塾跟我学系列——JAVA篇 第五章 Java高级类特性

原创 Linux操作系统 作者:今天天气好晴朗 时间:2012-02-03 16:14:42 0 删除 编辑

教学目标:

i掌握Java中的继承

i掌握方法的覆盖

i掌握方法的重载

i掌握Java中的多态

i掌握staticfinal的特性和使用

i理解Java的内存分配

 


一、Java中的继承                                                     

1.extends关键字                                                                

前面我们已经学习过了什么是继承,那么在Java里面如何来表达继承的关系呢?就是使用extends关键字,比如:经理这个类继承雇员这个类,示例如下:

public class Employee {  //雇员

  String name;       //姓名

  Date hireDate;     //雇佣日期

  Date dateOfBirth;  //生日

  String jobTitle;   //职位

  int grade;

  //...

}

public class Manager extends Employee {

  String department;          //部门

  Employee[] subordinates;    //下属

  //...

}

在这样的定义中,Manager类被定义,具有 Employee 所拥有的所有变量及方法。所有这些变量和方法都是从父类的定义中继承来的。所有的程序员需要做的是定义额外特征或规定将适用的变化。

 

注意:这种方法是在维护和可靠性方面的一个伟大进步。如果在Employee类中进行修改,那么,Manager类就会自动修改,而不需要程序员做任何工作,除了对它进行编译。

 

2.初始化子类必先初始化父类                                                     

在Java编程语言中,对象的初始化是非常结构化的,这样做是为了保证安全。在前面的模块中,看到了当一个特定对象被创建时发生了什么。由于继承性,对象被完成,而且下述行为按顺序发生:

    (1)存储空间被分配并初始化到0值

    (2)进行显式初始化

    (3)调用构造方法

(4)层次中的每个类都会发生最后两个步骤,是从最上层开始。Java技术安全模式要求在子类执行任何东西之前,描述父类的一个对象的各个方面都必须初始化。因此,Java编程语言总是在执行子构造方法前调用父类构造方法的版本。

有继承的类在运行的时候,一定要记得:初始化子类必先初始化父类,这是Java程序的一个基本运行过程。比如:

1 public class Child extends Parent{

2   private String name = "Java私塾";

3   private int age = 2;

4   public Child(){

5      age = 1000; //期望能到1000年,呵呵

6   }

7   public static void main(String[] args) {

8      Child t = new Child();   //创建子类的对象

9      System.out.println(t.name+"的年龄是"+t.age+"年");

10  }  

11}

12class Parent{

13  private int num = 1;

14  public Parent(){

15     System.out.println("现在初始化父类");

16  }

17  public void test(){

18     System.out.println("这是父类的test方法");

19  }

20}

 

上述类的基本运行顺序是:

(1) 先运行到第7行,这是程序的入口

(2) 然后运行到第8行,这里要new一个Child,就要调用Child的构造方法

(3) 就运行到第4行,注意:初始化子类必先初始化父类

(4) 要先初始化父类,所以运行到第14行

(5) 然后是第13行,初始化一个类,必须先初始化它的属性

(6) 然后是第15行

(7) 然后是第16行,表示父类初始化完成

(8) 然后是回到子类,开始初始化属性,因此运行到第2行,然后是第3行

(9) 子类属性初始化完过后,才回到子类的构造方法,执行里面的代码,也就是第5行

(10) 然后是第6行,表示new一个Child实例完成

(11) 然后回到main方法中执行第9行

(12) 然后是第10行

 

运行结果是:

现在初始化父类

Java私塾的年龄是1000年

 

3.单继承性                                                                     

单继承性:当一个类从一个唯一的类继承时,被称做单继承性。单继承性使代码更可靠。接口提供多继承性的好处,而且没有(多继承的)缺点。

Java编程语言允许一个类仅能继承一个其它类,即一个类只能有一个父类。这个限制被称做单继承性。单继承性与多继承性的优点是面向对象程序员之间广泛讨论的话题。Java编程语言加强了单继承性限制而使代码更为可靠,尽管这样有时会增加程序员的工作。后面会学到一个被叫做接口(interface)的语言特征,它允许多继承性的大部分好处,而不受其缺点的影响。

 

 

 

 

使用继承性的子类的一个例子如图所示:

 

 

 

 

 

 

 

 

 

 

 

 

 


4.构造方法不能被继承                                                            

    尽管一个子类从父类继承所有的方法和变量,但它不继承构造方法,掌握这一点很重要。

一个类能得到构造方法,只有两个办法。或者写构造方法,或者根本没有写构造方法,类有一个默认的构造方法。

 

5.关键字super                                                                  

关键字super可被用来引用该类的父类,它被用来引用父类的成员变量或方法。父类行为被调用,就好象该行为是本类的行为一样,而且调用行为不必发生在父类中,它能自动向上层类追溯。

 

super关键字的功能:

(1)点取父类中被子类隐藏了的数据成员

(2)点取已经覆盖了的方法

(3)作为方法名表示父类构造方法

例如:

public class Employee {

  private String name;

  private int salary;

public String getDetails() {

      return "Name: " + name + "\nSalary: " + salary;

  }

}

public class Manager extends Employee {

  private String department;

 

  public String getDetails() {

    return super.getDetails() + // 调用父类的方法

                           "\nDepartment: " + department;

  }

}

请注意,super.method()格式的调用,如果对象已经具有父类类型,那么它的方法的整个行为都将被调用,也包括其所有负面效果。该方法不必在父类中定义,它也可以从某些祖先类中继承。也就是说可以从父类的父类去获取,具有追溯性,一直向上去找,直到找到为止,这是一个很重要的特点。

 

6.调用父类构造方法                                                                 

在许多情况下,使用默认构造方法来对父类对象进行初始化。

当然也可以使用super来显示调用父类的构造方法。

 

public class Employee {

  String name;

  public Employee(String name) {

    this.name = name;

  }

}

 

public class Manager extends Employee {

  String department;

  public Manager(String name, String department) {

    super(name);

    this.department = department;

  }

}

 

注意:无论是super(…)还是this(…),都必须放在构造方法的第一句。(所以不会同时出现在一个方法中的,也没有这个必要。)

 

通常要定义一个带参数的构造方法,并要使用这些参数来控制一个对象的父类部分的构造。可能通过从子类构造方法的第一行调用关键字super的手段调用一个特殊的父类构造方法作为子类初始化的一部分。要控制具体的构造方法的调用,必须给super()提供合适的参数。当不调用带参数的super时,缺省的父类构造方法(即,带0个参数的构造方法)被隐含地调用。在这种情况下,如果没有缺省的父类构造方法,将导致编译错误。

public class Employee {

  String name;

  public Employee(String name) {

    this.name = name;

  }

}

public class Manager extends Employee {

  String department;

  public Manager(String name, String department) {

    super(name); // 调用父类参数为String类型的构造方法

    this.department = department;

  }

}

当被使用时,super或this的方法调用必须被放在构造方法的第一句。显然,两者不能被放在一个单独行中,但这种情况事实上不是一个问题。如果写一个构造方法,它既没有调用super(…)也没有调用this(…),编译器自动插入一个调用到父类构造方法中,而不带参数。其它构造方法也能调用super(…)或this(…),调用一个static方法和构造方法的数据链。最终发生的是父类构造方法(可能几个)将在链中的任何子类构造方法前执行。

 

二、方法的覆盖和重载                                         

1.方法的覆盖                                                                    

1.1 什么是方法的覆盖(override)?

    在类继承中,子类可以修改从父类继承来的行为,也就是说子类能创建一个与父类方法有不同功能的方法,但具有相同的:名称、返回类型、参数列表

如果在新类中定义一个方法,其名称、返回类型及参数表正好与父类中方法的名称、返回类型及参数相匹配,那么,新方法被称做覆盖旧方法。方法覆盖也叫作重写。

 

1.2 示例

如下在Employee和Manager类中的这些方法:

public class Employee {

  String name;

  int salary;

 

  public String getDetails() {

    return " Name: " + name + " \n " +  "Salary: " + salary;

  }

}

 

public class Manager extends Employee {

  String department;

 

  public String getDetails() {

    return " Name: " + name + " \n " +  " Manager of " + department;

  }

}

Manager类有一个定义的getDetails()方法,因为它是从Employee类中继承的。基本的方法被子类的版本所代替或覆盖了。

 

1.3 到底运行哪一个方法?

这里会给我们带来一个麻烦,父子类中有相同的方法,那么在运行时到底调用哪一个方法呢?假设下述方案:

Employee e = new Employee();

Manager m = new Manager();

   如果请求e.getDetails()和m.getDetails(),就会调用不同的行为。Employee对象将执行与Employee有关的getDetails版本,Manager对象将执行与Manager有关的getDetails()版本。

   不明显的是如下所示:

Employee e = new Manager();

e.getDetails();

    或某些相似效果,比如一个通用方法参数或一个来自异类集合的项。

    事实上,你得到与变量的运行时类型(即,变量所引用的对象的类型)相关的行为,而不是与变量的编译时类型相关的行为。这是面向对象语言的一个重要特征。它也是多态性的一个特征,并通常被称作虚拟方法调用。

在前例中,被执行的e.getDetails()方法来自对象的真实类型,Manager。

因此规则是:编译时看数据类型(任何时候都对),运行时看实际的对象类型(new操作符后跟的构造方法是哪个类的)。一句话:new谁就调用谁的方法。(只有在方法覆盖时才是这样的。)

 

1.4 覆盖方法的规则

    记住,子类的方法的名称以及子类方法参数的顺序必须与父类中的方法的名称以及参数的顺序相同,以便该方法覆盖父类版本。下述规则适用于覆盖方法:

(1)覆盖方法的返回类型、方法名称、参数列表必须与它所覆盖的方法的相同。

(2)覆盖方法不能比它所覆盖的方法访问性差(即访问权限不允许缩小)。

(3)覆盖方法不能比它所覆盖的方法抛出更多的异常。

    这些规则源自多态性的属性和Java编程语言必须保证“类型安全”的需要。考虑一下这个无效方案:

public class Parent {

  public void method() {

  }

}

public class Child extends Parent {

  private void method() {//编译就会出错

  }

}

public class Test{

  public void otherMethod() {

    Parent p1 = new Parent();

    Parent p2 = new Child();

    p1.method();

    p2.method();

  }

}

 

Java编程语言语义规定,p2.method()导致方法的Child版本被执行,但因为方法被声明为private,p2(声明为Parent)不能访问它。于是,语言语义冲突。

 

2.方法的重载                                                                   

假如你必须在不同情况下发送不同的信息给同一个成员方法的话,该怎么办呢?你可以通过对此成员方法说明多个版本的方法来实现重载。重载的本质是创建了一个新的成员方法:你只需给它一个不同的参数列表。

 

2.1 什么是方法的重载(overload)?

在同一个Java类中(包含父类),如果出现了方法名称相同,而参数列表不同的情况就叫做重载。

参数列表不同的情况包括:个数不同、类型不同、顺序不同等等。特别提示,仅仅参数变量名称不同是不可以的。

 

2.2 重载示例

如下例所示:

void getArea(int w,int h);

void getArea(float w,float h);

在第二种情况下,成员方法getArea()接受两个浮点变量作为它的参数,编译器根据调用时的不同参数来决定该调用哪一种成员方法,假如你把两个整数提供给成员方法,就调用第一个成员方法;假如你把两个浮点数提供给成员方法,第二个成员方法就被调用。

当写代码来调用这些方法中的一个方法时,便以其会根据提供的参数的类型来选择合适的方法。

 

注意:跟成员方法一样,构造方法也可以重载。

 

 2.2 方法的重载的规则

(1)方法名称必须相同

(2)参数列表必须不同(个数不同,或类型不同,或参数排列顺序不同)。

(3)方法的返回类型可以相同也可以不相同。仅仅返回类型不同不足以成为方法的重载。

 

注意:调用语句的参数表必须有足够的不同,以至于允许区分出正确的方法被调用。正常的拓展晋升(如,单精度类型float到双精度类型double)可能被应用,但是这样会导致在某些条件下的混淆。

 

2.3 比较方法的覆盖和重载

方法重载:

在一个类(或父子类)中用相同的名字创建多个方法(每个方法的参数表不同)

方法覆盖:

在一个类中创建的方法与父类中方法的名字、返回类型和参数表相同,覆盖是针对两个类说的,而且必须是子类(或孙类,孙孙类等)覆盖掉父类的方法

 

三、Java中的多态                                            

1.多态是什么?                                                                    

多态是同一个行为具有多个不同表现形式或形态的能力。

将经理描述成职员不只是描述这两个类之间的关系的一个简便方法。回想一下,经理类具有父类职员类的所有属性、成员和方法。这就是说,任何在Employee上的合法操作在Manager上也合法。如果Employee 有raiseSalary()和fire()两个方法,那么Manager 类也有。

在这种Manager继承Employee的情况下,一个Employee既可以是一个普通的Employee类,也可以是一个Manager类。也就是说下述表示都是对的:

Employee  e = new  Employee();

Employee  e = new  Manager();

 

从上面可以看到:同一个行为Employee具有多个不同的表现形式(既可以是一个普通的Employee类,也可以是一个Manager类),这就被称为多态。

 

注意:方法没有多态的说法,严格说多态是类的特性。但是也有对方法说多态的,了解一下,比如前面学到的方法覆盖称为动态多态,是一个运行时问题;方法重载称为静态多态,是一个编译时问题。

 

2.多态与类型                                                                  

一个对象只有一个格式(是在构造时给它的)。但是,既然变量能指向不同格式的对象,那么变量就是多态性的。也就是说一个对象只有一种形式,但一个变量却有多种不同形式

    象大多数面向对象语言一样,Java实际上允许父类类型的引用变量指向一个子类的对象。因此,可以说:

Employee e = new Manager()

    使用变量e是因为,你能访问的对象部分只是Employee的一个部分;Manager的特殊部分是隐藏的。这是因为编译者应意识到,e 是一个Employee,而不是一个Manager。因而,下述情况是不允许的:

e.department = " Finance " ; //非法的,编译时会出错

 

可能有的人会不理解,为什么明明是new的一个Manager,却不能访问Manager的属性数据。原因在于编译的时候,变量e是一个Employee的类型,编译器并不去管运行时e指向的具体对象是一个Employee的对象,还是一个Manager的对象,所以它只能访问到Employee里面定义的属性和方法。所以说编译时看数据类型。

那么要想访问到Manager里面的department该怎么办呢?这就需要先对e进行强制类型转换,把它还原成为Manager类型,就可以访问到Manager里面的属性和方法了,如下:

Employee e = new Manager();

Manager m = (Manager)e;

m.department = “开发部”; //这就是合法的了

 

3.instanceof运算符                                                             

多态性带来了一个问题:如何判断一个变量所实际引用的对象的类型。C++使用runtime-type information(RTTI),Java使用instanceof操作符。

    instanceof运算符功能:用来判断某个实例变量是否属于某种类的类型。一旦确定了变量所引用的对象的类型后,可以将对象恢复给对应的子类变量,以获取对象的完整功能。

示例如下:

public class Employee extends Object     //extends Object不要输入,这里是提示

public class Manager extends Employee

public class Contractor extends Employee

 

如果通过Employee类型的引用接受一个对象,它变不变成Manager或Contractor都可以。可以象这样用instanceof 来测试:

 

public class Employee extends Object

public class Manager extends Employee

public class Contractor extends Employee

 

public void method(Employee e) {

  if (e instanceof Manager) {

    //如果雇员是经理,可以做的事情写在这里

  }else if (e instanceof Contractor) {

    //如果雇员是普通的职员,可以做的事情写在这里

  }else {

    //说明是临时雇员,可以做的事情写在这里

  }

}

 

4.多态对象的类型转换                                                              

在你接收父类的一个引用时,你可以通过使用instanceof运算符判定该对象实际上是你所要的子类,并可以用类型转换该引用的办法来恢复对象的全部功能。

public void method(Employee e) {

  if (e instanceof Manager) {

    Manager m = (Manager)e;

    System.out.println( " This is the manager of " + m.department);

  }

  // rest of operation

}

    如果不用强制类型转换,那么引用e.department的尝试就会失败,因为编译器不能将被称做department的成员定位在Employee类中。

    如果不用instanceof做测试,就会有类型转换失败的危险。通常情况下,类型转换一个对象引用的尝试是要经过几种检查的:

    向上强制类型转换类层次总是允许的,而且事实上不需要强制类型转换运算符。可由简单的赋值实现。

    严格讲不存在向下类型转换,其实就是强制类型转换,编译器必须满足类型转换至少是可能的这样的条件。比如,任何将Manager引用类型转换成Contractor引用的尝试是肯定不允许的,因为Contractor不是一个Manager。类型转换发生的类必须是当前引用类型的子类。

    如果编译器允许类型转换,那么,该引用类型就会在运行时被检查。比如,如果instanceof检查从源程序中被省略,而被类型转换的对象实际上不是它应被类型转换进去的类型,那么,就会发生一个运行时错误(exception)。异常是运行时错误的一种形式,而且是后面章节的主题。

5.动态绑定                                                                       

绑定:将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)。

动态绑定:当给对象发送请求时,所引起的具体操作既与请求本身有关又与接受对象有关。支持相同请求的不同对象可能对请求激发的操作有不同的实现。发送给对象的请求和它的相应操作在运行时刻的连接就称之为动态绑定(dynamic binding)。

动态绑定是指发送的请求直到运行时刻才受你的具体的实现的约束。因而,在知道任何有正确接口的对象都将接受此请求时,你可以写一个一般的程序,它期待着那些具有该特定接口的对象。进一步讲,动态绑定允许你在运行时刻彼此替换有相同接口的对象。这种可替换性就称为多态(polymorphism),它是面向对象系统中的核心概念之一。多态允许客户对象仅要求其它对象支持特定接口,除此之外对其假设几近于无。多态简化了客户的定义,使得对象间彼此独立,并可以在运行时刻动态改变它们相互的关系。

 

若在程序运行以前执行绑定(由编译器和链接程序,如果有的话),就叫作“早期绑定”。但是在只有一个Instrument句柄的前提下,编译器不知道具体该调用哪个方法。

  解决的方法就是“后期绑定”,它意味着绑定在运行期间进行,以对象的类型为基础。后期绑定也叫作“动态绑定”或“运行期绑定”。若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。

  Java中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成final。这意味着我们通常不必决定是否应进行后期绑定——它是自动发生的。

 

四、static                                                 

1.static修饰符                                                                       

    static修饰符能够与属性、方法和内部类一起使用,表示是“静态”的。

类中的静态变量和静态方法能够与“类名”一起使用,不需要创建一个类的对象来访问该类的静态成员。所以static修饰的变量又称作“类变量”。这与实例变量不同。实例变量总是用对象来访问,因为它们的值在对象和对象之间有所不同。

下列示例展示了如何访问一个类的静态变量:

class StaticModifier {

    static int i = 10;

    int j;

 

    StaticModifier() {

       j = 20;

    }

}

public class Test {

    public static void main(String args[]) {

       System.out.println("类变量 i=" + StaticModifier.i);

       StaticModifier s = new StaticModifier();

       System.out.println("实例变量 j=" + s.j);

    }

}

上述程序的输出是:

类变量i=10   

实例变量j=20

 

2.static属性的内存分配                                                               

在上面的例子中,无需创建类的对象即可访问静态变量i。之所以会产生这样的结果,是因为编译器只为整个类创建了一个静态变量的副本,因此它能够用类名进行访问。也就是说:一个类中,一个static变量只会有一个内存空间,虽然有多个类实例,但这些类实例中的这个static变量会共享同一个内存空间。记住:static只存一份。

示例如下:

public class Test{

    static UserModel um = new UserModel();

    public static void main(String[] args) {

       Test t1 = new Test();

       t1.um.userName = "张三";

       Test t2 = new Test();

       t2.um.userName = "李四";

      

       System.out.println("t1.um.userName=="+t1.um.userName);

       System.out.println("t2.um.userName=="+t2.um.userName);

    }

}

class UserModel{

    public String userName="";

}

运行结果:

t1.um.userName==李四

t2.um.userName==李四

为什么会是一样的值呢?就是因为多个实例中的静态变量um是共享同一内存空间, t1.um和t2.um其实指向的都是同一个内存空间,所以就得到上面的结果了。

要想看看是不是static导致这样的结果,你可以尝试去掉UserModel前面的static,然后再试一试,看看结果,应该如下:

t1.um.userName==张三

t2.um.userName==李四

 

还有一点也很重要:static的变量是在类装载的时候就会被初始化。也就是说,只要类被装载,不管你是否使用了这个static变量,它都会被初始化。

小结一下:类变量(class variables)用关键字static修饰,在类加载的时候,分配类变量的内存,以后在生成类的实例对象时,将共享这块内存(类变量),任何一个对象对类变量的修改,都会影响其它对象。外部有两种访问方式:通过对象来访问或通过类名来访问。

 

3.static的基本规则                                                            

    有关静态变量或方法的一些要点如下:

l         一个类的静态方法只能访问静态属性

l         一个类的静态方法不能够直接调用非静态方法

l         如访问控制权限允许,static属性和方法可以使用对象名加‘.’方式调用;当然也可以使用实例加‘.’方式调用

l         静态方法中不存在当前对象,因而不能使用“this”,当然也不能使用”super”;

l         静态方法不能被非静态方法覆盖;

l         构造方法不允许声明为static的

 

static方法可以用类名而不是引用来访问,如:

public class GeneralFunction {

             public static int addUp(int x, int y) {

               return x + y;

             }

           }

           public class UseGeneral {

             public void method() {

               int a = 9;

               int b = 10;

               int c = GeneralFunction.addUp(a, b);

               System.out.println("addUp() gives " + c);

             }

           }

    因为static方法不需它所属的类的任何实例就会被调用,因此没有this值。结果是,static方法不能访问与它本身的参数以及static变量之外的任何变量,访问非静态变量的尝试会引起编译错误。

注: 非静态变量只限于实例,并只能通过实例引用被访问。

 

4.静态初始器——静态块                                                          

4.1 什么是静态初始器?

静态初始化器(Static Initializer)是一个存在与类中方法外面的静态块。静态初始器仅仅在类装载的时候(第一次使用类的时候)执行一次。

静态初始化器的功能是:往往用来初始化静态的类属性。

 

4.2 示例

class Count {

    public static int counter;

    static {//只运行一次

       counter = 123;

       System.out.println("Now in static block.");

    }

    public void test(){

       System.out.println("test method=="+counter);

    }

}

public class Test {

    public static void main(String args[]) {

       System.out.println("counter=" + Count.counter);

       new Count().test();

    }

}

运行结果是:

Now in static block.

counter=123

test method==123

 

5.静态import                                                                     

当我们要获取一个随机数时,写法是:

 public class Test {

    public static void main(String[] args) {

       double randomNum = Math.random();

       System.out.println("the randomNum=="+randomNum);

    }

}

从JDK5.0开始可以写为:

import static java.lang.Math.random;

public class Test { 

    public static void main(String[] args) {

       double randomNum = random();

       System.out.println("the randomNum=="+randomNum);

    }

}

  静态引用使我们可以象调用本地方法一样调用一个引入的方法,当我们需要引入同一个类的多个方法时,只需写为“import static java.lang.Math.*”即可。这样的引用方式对于枚举也同样有效。

 

五、final                                                      

1.final修饰符                                                                        

在Java中声明类、属性和方法时,可使用关键字final来修饰。final所标记的成分具有“终态”的特征,表示“最终的”意思。

 

2.final的规则

其具体规定如下:

 

l         final标记的类不能被继承。

l         final标记的方法不能被子类重写。

l         final标记的变量(成员变量或局部变量)即成为常量,只能赋值一次。

l         final标记的成员变量必须在声明的同时赋值,如果在声明的时候没有赋值,那么只有一次赋值的机会,而且只能在构造方法中显式赋值,然后才能使用。

l         final标记的局部变量可以只声明不赋值,然后再进行一次性的赋值。

l         final一般用于标记那些通用性的功能、实现方式或取值不能随意被改变的成分,以避免被误用,

 

例如实现数学三角方法、幂运算等功能的方法,以及数学常量π=3.141593、e=2.71828等。事实上,为确保这终态性,提供了上述方法和常量的java.lang.Math类也已被定义为final的。

需要注意的是,如果将引用类型(即,任何类的类型)的变量标记为final,那么该变量不能指向任何其它对象。但可以改变对象的内容,因为只有引用本身是final的。

如果变量被标记为final,其结果是使它成为常数。想改变final变量的值会导致一个编译错误。下面是一个正确定义final变量的例子:

public final int MAX_ARRAY_SIZE = 25;

 

例:final关键字程序:Test.java

public final class Test{

    public static final int TOTAL_NUMBER= 5 ;

    public int id;

    public Test(){

       id = ++TOTAL_NUMBER;//非法,对final变量TOTAL_NUMBER进行二次赋值了。

       //因为++TOTAL_NUMBER相当于:TOTAL_NUMBER=TOTAL_NUMBER+1

    }

    public static void main(String[] args) {

       final Test t = new Test();

       final int i= 10;

       final int j;

       j = 20;

       j = 30; //非法,对final变量进行二次赋值

    }

}

Java编程语言允许关键字final被应用到类上(放在class关键字前面)。如果这样做了,类便不能被再派生出子类。比如,类Java.lang.String就是一个final类。这样做是出于安全原因,因为它保证,如果方法有字符串的引用,它肯定就是类String的字符串,而不是某个其它类的字符串,这个类是String的被修改过的子类,因为String可能被恶意窜改过。

方法也可以被标记为final。被标记为final的方法不能被覆盖。这是由于安全原因。如果方法具有不能被改变的实现,而且对于对象的一致状态是关键的,那么就要使方法成为final。被声明为final的方法有时被用于优化。编译器能产生直接对方法调用的代码,而不是通常的涉及运行时查找的虚拟方法调用。

.

六、再谈Java内存分配                                      

Java程序运行时的内存结构分成:方法区、栈内存、堆内存、本地方法栈几种。

栈和堆都是数据结构的知识,如果不清楚,没有关系,就当成一个不同的名字就好了,下面的讲解不需要用到它们具体的知识。

1.方法区                                                                        

方法区存放装载的类数据信息包括:

(1):基本信息:

1)每个类的全限定名

2)每个类的直接超类的全限定名(可约束类型转换)

3)该类是类还是接口

4)该类型的访问修饰符

5)直接超接口的全限定名的有序列表

(2):每个已装载类的详细信息:

1)运行时常量池:

存放该类型所用的一切常量(直接常量和对其它类型、字段、方法的符号引用),它们以数组形式通过索引被访问,是外部调用与类联系及类型对象化的桥梁。它是类文件(字节码)常量池的运行时表示。(还有一种静态常量池,在字节码文件中)。

2)字段信息:

类中声明的每一个字段的信息(名,类型,修饰符)。

3)方法信息:

类中声明的每一个方法的信息(名,返回类型,参数类型,修饰符,方法的字节码和异常表)。

4)静态变量

5)到类classloader的引用:即到该类的类装载器的引用。

6)到类class的引用:

虚拟机为每一个被装载的类型创建一个class实例,用来代表这个被装载的类。

 

2.栈内存                                                                           

Java栈(stack)内存以帧的形式存放本地方法的调用状态(包括方法调用的参数,局部变量,中间结果等)。每调用一个方法就将对应该方法的方法帧压入Java栈,成为当前方法帧。当调用结束(返回)时,就弹出该帧。

编译器将源代码编译成字节码(.class)时,就已经将各种类型的方法的局部变量,操作数栈大小确定并放在字节码中,随着类一并装载入方法区。当调用方法时,通过访问方法区中的类的信息,得到局部变量以及操作数栈的大小。

也就是说:在方法中定义的一些基本类型的变量和对象的引用变量都在方法的栈内存中分配。   当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作它用。

 

栈内存的构成:

Java栈内存由局部变量区、操作数栈、帧数据区组成。

(1)局部变量区为一个以字为单位的数组,每个数组元素对应一个局部变量的值。调用方法时,将方法的局部变量组成一个数组,通过索引来访问。若为非静态方法,则加入一个隐含的引用参数this,该参数指向调用这个方法的对象。而静态方法则没有this参数。因此,对象无法调用静态方法。

(2)操作数栈也是一个数组,但是通过栈操作来访问。所谓操作数是那些被指令操作的数据。当需要对参数操作时如a=b+c,就将即将被操作的参数压栈,如将b和c压栈,然后由操作指令将它们弹出,并执行操作。虚拟机将操作数栈作为工作区。

(3) 帧数据区处理常量池解析,异常处理等

 

3.堆内存                                                                       

堆(heap)内存用来存放由new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。

 

栈内存和堆内存比较

栈与堆都是Java用来在内存中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。

Java的堆是一个运行时数据区,对象从中分配空间。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和对象句柄。

栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:

int a = 3;

int b = 3;

编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。内存示意图如下:

栈内存:

变量a

3

变量b

 

 

 

 

这时,如果再令a=4;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。此时的内存分配示意图如下:

 

栈内存:

变量a

3

变量b

4

 

 

 

 

 

而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。

 

4.本地方法栈内存                                                                       

与调用的本地(native)方法的语言相关,如调用的是一个c语言方法则为一个c栈。本地方法可以回调java方法。若有java方法调用本地方法,虚拟机就运行这个本地方法。

在虚拟机看来运行这个本地方法就是执行这个java方法,如果本地方法抛出异常,虚拟机就认为是这个java方法抛出异常。

Java通过Java本地接口JNI(Java Native Interface)来调用其它语言编写的程序,在Java里面用native修饰符来描述一个方法是本地方法。这个了解一下就好了,在我们的课程中不会涉及到。

 

5.String的内存分配                                                                      

String是一个特殊的包装类数据。可以用:

String str1 = new String("abc");

String str2 = "abc";

两种的形式来创建,第一种是用new()来新建对象的,它会在存放于堆中。每调用一次就会创建一个新的对象。

而第二种是先在栈中创建一个对String类的对象引用变量str,然后查找栈中有没有存放"abc",如果没有,则将"abc"存放进栈,并令str指向”abc”,如果已经有”abc” 则直接令str指向“abc”。

比较类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==,下面用例子说明上面的理论。
    String str1 = "abc";

String str2 = "abc";

System.out.println(str1==str2); //true

可以看出str1和str2是指向同一个对象的。

 

String str1 = new String ("abc");

String str2 = new String ("abc");

System.out.println(str1==str2); // false

用new的方式是生成不同的对象。每一次生成一个。

 

   因此用第一种方式创建多个”abc”字符串,在内存中其实只存在一个对象而已。这种写法有利于节省内存空间。同时它可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。

另一方面, 要注意: 我们在使用诸如String str = "abc";的格式时,总是想当然地认为,创建了String类的对象str。担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。只有通过new()方法才能保证每次都创建一个新的对象。

由于String类的值不可变性(immutable),当String变量需要经常变换其值时,应该考虑使用StringBuffer或StringBuilder类,以提高程序效率。

 

作业                                 

1.下列代码运行结果是什么?

public class Bool{

  static boolean b;

  public static void main(String []args){

int x=0;

if(b){

  x=1;

}

else if(b=false){

  x=2;

}

else if(b){

  x=3;

}

else{

  x=4;

}

System.out.println(“x= ”+x);

}

}

 

 

2.当编译和运行下列程序段时,会发生什么?

class Person {}

class Woman extends Person {}

class Man extends Person {}

public class Test

{

  public static void main(String argv[]){

Man m=new Man();

Woman w=(Woman) new Man();

   }

}

A 通过编译和并正常运行。

B 编译时出现例外。

C 编译通过,运行时出现例外。

D 编译不通过

 

3.对于下列代码:

   1  class Person {

  2    public void printValue(int i, int j) {//... }

  3    public void printValue(int i){//... }

  4  }

  5  public class Teacher extends Person {

  6    public void printValue() {//... }

  7    public void printValue(int i) {//...}

  8    public static void main(String args[]){

  9      Person t = new Teacher();

  10     t.printValue(10);

  11   }

       12 }

  

第10行语句将调用哪行语句?

A line 2

B line 3

C line 6

D line 7

 

4.下列代码运行结果是什么?

public class  A{

  puvlic static void main(Stirng []args){

int m=2;

int p=1;

int t=0;

for(;p<5;p++){

  if(t++>m){

  m=p+t;

}

}

System.out.println(“t equals ”+t);

}

}

A.      2

B.      4

C.      6

D.     7

 

5.完成此段代码可以分别添加哪两个选项?

1.        public class Test{

2.         

3.        public static void main(String []args){

4.          

5.          System.out.println(“c=”+c);

6.        }

}

a)         在第2行加上语句static char c;

b)        在第2行加上语句char c;

c)        在第4行加上语句static char c;

d)        在第4行加上语句char c=’f’;

 

6.将下面类中的变量和方法改为静态的, 使程序能正确编译执行。如果保持用实例变量和方法就必须创建对象,请创建A的对象并通过该对象来引用实例变量和方法。

public class A

{

       int a=9;

              public void show(int a)

              {

              System.out.println(a*10);

       }

      public static void main(String args[]){

       a+=a;

       show(a);

}

}

 

7.回答final属性和final的局部变量在初始化方面分别有哪些规定,并上机验证。

 

8.代码运行结果是什么?

public class Test{

  public static void main(String []args){

double num=7.4;

int a=(int)Math.abs(num+0.5);

int b=(int)Math.ceil(num+0.5);

int c=(int)Math.floor(num+0.5);

int d=(int)Math.round(num+0.5);

int e=(int)Math.round(num-0.5);

int f=(int)Math.floor(num-0.5);

int g=(int)Math.ceil(num-0.5);

int h=(int)Math.abs(num-0.5);

 

System.out.println("a="+a);

System.out.println("b="+b);

System.out.println("c="+c);

System.out.println("d="+d);

System.out.println("e="+e);

System.out.println("f="+f);

System.out.println("g="+g);

System.out.println("h="+h); }

}

 

以下是编程题:

1.编写MyPoint的一个子类MyXYZ,表示三维坐标点,重写toString方法用来显示这个对象的x、y、z的值,如显示(1,2,3),最后用main方法测试

 

2.创建Rodent(啮齿动物):Mouse(老鼠),Gerbil(鼹鼠),Hamster(大颊鼠)等的一个继承分级结构。在基础类中,提供适用于所有Rodent 的方法,并在衍生类中覆盖它们,从而根据不同类型的Rodent 采取不同的行动。创建一个Rodent 数组,在其中填充不同类型的Rodent,然后调用自己的基础类方法,看看会有什么情况发生。

 

Java私塾跟我学系列——JAVA  网址:http://www.javass.cn  电话:010-68434236

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/26660100/viewspace-715597/,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2012-02-02

  • 博文量
    65
  • 访问量
    42770