Java核心技术第5章(2)

5.1.1   继承层次

    继承并不仅限于一个层次.例如,可以由Manager类派生Executive类.由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy).在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链.
    通常,一个祖先类可以拥有多个子孙继承链.
    注释:Java不支持多继承.

5.1.2   多态

    有一个用来判断是否应该设计为继承关系的简单规则,这就是"is-a"规则,它表明了类的每个对象也是超类的对象.例如,每个经理都是雇员,因此,将Manager类设计为Employee类的子类是显而易见的.
    "is-a"规则的另一种表述法是置换法则.它表明程序中出现超类对象的任何地方都可以用子类对象置换.
    例如,可以将一个子类的对象赋给超类变量.
Employee e;
e = new Employee(...);      // Employee object expected
e = new Manager(...);       // ok! Manager can be used as well  
    在Java程序语言中,对象变量是多态的.一个Employee变量既可以引用一个Employee类对象,也可以引用一个Employee类的任何一个子类的对象.
    在程序5-1的ManagerTest.java中,已经看到了置换法则的优点:
Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss;
    在这个例子中, 变量staff[0]与boss引用同一个对象,但编译器将staff[0]看成Employee对象.
    这意味着,可以这样调用
boss.setBonus(5000);        // ok
    但不能这样调用
staff[0].setBonus(5000);    // error
    这是 因为staff[0]声明的类型是Employee,而setBonus不是Employee类的方法.
    然而,不能将一个超类的引用赋给子类变量.例如,下面的赋值时非法的
Manager m = staff[i];       // error
    原因很清楚:不是所有的雇员都是经理.如果赋值成功,m有可能引用了一个不是经理的Employee对象,当在后面调用m.setBonus(...)时就可能发生运行时错误.
    警告:在Java中,子类数组的引用可以转换成超类数组的引用,而不需要采用强制类型转换.例如,下面是一个经理数组
Manager[] managers = new Manager[20];
    将它转换成Employee[]数组完全是合法的:
Employee[] staff = managers;        // ok
    这样做肯定不会有问题.然而,实际上,将会发生一些令人惊讶的事情.要切记managers和staff引用的是同一个数组.现在看一下这条语句:
staff[0] = new Employee("Harry", ...);
    编译器竟然接纳了这个赋值操作,但在这里staff[0]与manager[0]引用的是同一个对象,当调用managers[0].setBonus(1000)的时候,将会导致调用一个不存在的实例域,进而搅乱相邻存储空间的内容.
    为了确保不发生这类错误,所有数组都要牢记创建它们的元素类型,并负责监督仅将类型兼容的引用存储到数组中.

5.1.3   动态绑定

    弄清调用对象方法的执行过程十分重要, 下面是调用过程的详细描述:
    1.编译器查看对象的声明类型和方法名.
假设调用x.f(param),且隐式参数x声明为C类的对象.需要注意的是:有可能存在多个名字为f,但参数类型不一致的方法.例如,可能存在方法f(int)和方法f(String).编译器将会一一列举所有C类中名为f的方法和其超类中访问属性为 public 且名为f的方法(超类中的私有方法不可访问).
    至此,编译器已获得所有可能被调用的候选方法.
    2.接下来,编译器将查看调用方法时提供的参数类型.如果在所有名为f的方法中存在一个与提供的参数类型完全匹配就选择这个方法.这个过程被称为重载解析(overloading resolution).例如对于调用x.f("hello"),编译器将会挑选f(String),而不是f(int).由于允许类型转换(int 可以转换成 double,Manager可以转换成Employee),所以这个过程可能很复杂.如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误.
    至此,编译器已获得需要调用的方法名字和参数类型.
    注释:前面曾经说过,方法的名字和参数列表称为方法的签名.例如f(int)和f(String)是两个具有相同名字,不同签名的方法.如果在子类中定义了一个与超类签名相同的方法,那么子类中的这个方法就覆盖了超类中的这个相同签名的方法.
    不过,返回类型不是签名的一部分,因此,在覆盖方法时,一定要保证返回类型的兼容性.语序子类将覆盖方法的返回类型定义为原返回类型的子类型.例如,假设Employee类有
public Employee getBuddy() { ... }
    管理人员没有这种员工,为了反映这一点,在后面的子类Manager中,可以按照如下所示的方式覆盖这个方法
public Manager getBuddy() { ... }       // ok to change return type
    3.如果是 private 方法,static 方法,final 方法 或构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定(static binding).与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定.在上面个列举的示例中,编译器采用动态绑定的方式生成一条调用f(String)的指令.
    4.当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法.假设x的实际类型是D,它是C类的子类.如果D类定义了方法f(String),就直接调用它;否则,将在D类的超类中寻找f(String),以此类推.
    每次调用方法都要进行搜索,时间开销相当大.因此, 虚拟机预先为每个类创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法.这样一来,在真正调用方法的时候,虚拟机仅仅查找这个表就可以了.在前面的示例中,虚拟机搜索D类的方法表,以便寻找与调用f(String)相匹配的方法.这个方法既有可能是D.f(String),也有可能是X.f(String),这里的X是D的超类.这里需要提醒一点,如果调用 super.f(param),编译器将对隐式参数超类的方法表进行搜索.
    现在,查看一下程序5-1中ManagerTest.java的e.getSalary()的详细过程.e声明为Employee类型.Employee类只有一个名叫getSalary的方法,这个方法没有参数.因此,在这里不必担心重载解析的问题.
    由于getSalary不是 private 方法,static 方法或 final 方法,所以将采用动态绑定.虚拟机为Employee和Manager两个类生成方法表.在Employee的方法表中,列出了这个类定义的所有方法:
Employee:
    getName() -> Employee.getName()
    getSalary() -> Employee.getSalary()
    getHireDay() -> Employee.getHireDay()
    raiseSalary(double) -> Employee.raiseSalary(double)
    实际上,上面列出的方法并不完整,Employee类有一个超类Object,Employee类从这个超类中ahi继承了许多方法.
    Manager方法表稍微有些不同,其中有三个方法是继承而来的,一个方法是重新定义的,还有一个方法是新增加的.
Manager:
    getName() -> Employee.getName()
    getSalary() -> Manager.getSalary()
    getHireDay() -> Employee.getHireDay()
    raiseSalary(double) -> Employee.raiseSalary(double)
    setBonus(double) -> Manager.setBonus(double)
    在运行的时候,调用e.getSalary()的解析过程为:
    1.
首先,虚拟机提取e的实际类型的方法表.既可能使Employee,Manager的方法表,也可能是Employee类的其他子类的方法表.
    2.接下来,虚拟机搜索定义getSalary签名的类.此时,虚拟机已经知道应该调用哪个方法.
    3.最后,虚拟机调用方法
    动态绑定有一个非常重要的特性:无需对现存的代码进行修改,就可以对程序进行扩展.假设增加一个新类Executive,并且变量e有可能引用这个类的对象,不需要对包含调用e.getSalary()的代码进行重新编译.如果e恰好引用一个Executive类的对象,就会自动地调用Executive.getSalary()方法.
    警告:在覆盖一个方法的时候,子类方法不能低于超类方法的可见性.特别是,如果超类方法是 public,子类方法一定要声明为 public .经常会发生这类错误:在声明子类方法的时候,遗漏了 public 修饰符.此时编译器将会把它解释为试图降低访问权限.

5.1.4   阻止继承:final 类和方法

    有时候,可能希望 阻止人们利用某个类定义子类.不允许扩展的类被称为 final 类.如果在定义类的时候使用了 final 修饰符就表明这个类是 final 类.例如,假设希望阻止人们定义Executive类的子类,就可以在定义这个类的时候,使用 final 修饰符表明.声明格式如下所示:
final class Executive extends Manager
{
    ...
}
    类中的特定方法也可以被声明为 final .如果这样做, 子类就不能覆盖这个方法(final 类中的所有方法自动地成为 final 方法).例如
class Employee
{
    ...
    public final String getName()
    {
        return name;
    }
    ...
}
    注释:前面曾经说过,域也可以被声明为 final,对于 final 域来说,构造对象之后就不允许改变它的值了.不过将一个类声明为 final,只有其中的方法自动地成为 final,但不包括域.
    将方法或类声明为 final 的主要目的是:确保它们不会在子类中改变语义.String类就是 final 类,这意味着不允许任何人定义String的子类.换而言之,如果有一个String类额引用,它引用的一定是一个String对象,而不可能是其他类的对象.
    如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程被称为内联.例如,内联调用e.getName()将被替换为访问e.name域.这是一项很有意义的改进,因为CPU在处理调用方法的指令时,使用的分支转义会扰乱预取指令的策略,所以这被视为不受欢迎的.然而,如果getName在另外一个类中被覆盖,那么编译器就无法知道覆盖的代码将会做什么操作,因此也就不能对它进行内联处理了.
    虚拟机中的即时编译器比传统编译器的处理能力强得多,这种编译器可以准确地知道类之间的继承关系,并能够检测出类中是否真正地存在覆盖给定的方法.如果方法很简短,被频繁调用且没有真正地被覆盖,那么即时编译器就会将这个方法进行内联处理.

5.1.5   强制类型转换

    将一个类型强制转换成另外一个类型的过程被称为类型转换.Java程序设计语言提供了一种专门用于进行类型转换的表示法.例如:
double x = 3.405;
int nx = (int)x;
    将表达式x的值转换成整数类型,舍弃了小数部分.
    有时候需要将某个类的对象引用转换成另外一个类的对象引用.对象引用的转换语法与数值表达式的类型转换类型,仅需要用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了.例如:
Manager boss = (Manager)staff[0];
    进行类型转换的唯一原因是:在暂时忽略对象的实际类型之后,使用对象的全部功能.
    将数组中引用经理的元素复原成Manager类,以便能够访问新增加的所有变量.
    在Java中,每个对象变量都属于一个类型.类型描述了这个变量所引用的以及能够引用的对象类型.例如staff[i]引用一个Employee对象.
    将一个值存入变量时,编译器将检查是否允许该操作,将一个子类的引用赋给一个超类变量,编译器是允许的.但将一个超类的引用赋给一个子类变量,必须进行类型转换,这样才能够通过运行时的检查.
    如果试图在继承链上进行向下的类型转换并且"谎报"有关对象包含的内容,会发生什么情况呢?
Manager boss = (Manager)staff[1];       // error
    运行这个程序时,Java运行时系统将报告这个错误.应该养成这样一个良好的程序设计习惯: 在进行类型转换之前,先查看一下是否能够成功地转换.这个过程简单地 使用 instanceof 运算符就可以实现.例如:
if (staff[1] instanceof Manager)
{
    boss = (Manager)staff[1];
    ...
}
    最后,如果这个类型转换不可能成功,编译器就不会进行这个转换.
    综上所述:
    只能在继承层次内进行类型转换
    在将超类转换成子类之前,应该使用 instanceof 进行检查.
    注释
:如果x为 null,进行下列测试
x instanceof C
    不会产生异常,只是返回 false .
    一般情况下,应该尽量少用类型转换和 instanceof 运算符.
    注释:Java使用的类型转换语法来源于C语言"以往糟糕的日子",但处理过程却有些像C++的dynamic_cast操作.例如
Manager boss = (Manager)staff[1];       // Java
    等价于
Manager* boss = dynamic_cast<Manager*>(staff[1]);   // C++
    它们之间只有一点重要的区别:当类型转换失败时,Java不会生成一个 null 对象,而是抛出一个异常.从这个意义上讲,有点像C++中的引用(reference)转换.在C++中,可以在一个操作中完成类型测试和类型转换.
Manager* boss = dynamic_cast<Manager*>(staff[1]);   // C++
if (boss != NULL) ...
    而在Java中,需要将 instanceof 运算符和类型转换组合起来使用:
if (staff[1] instanceof Manager)
{
    Manager boss = (Manager)staff[1];
    ...
}

5.1.6   抽象类

    如果自下而上在类的继承层次结构中上移,位于上层的类更具有通用性,甚至可能更加抽象.
    考虑类Employee和类Student的父类Person,增加一个getDescription方法,它可以返回对一个人的简短描述.
    在Employee类和Student类中实现这个方法很容易.但是在Person类中应该理工什么内容呢?除了姓名之外,Person类一无所知.当然,可以让Person.getDescription()返回一个空字符串.然而,还有一个更好的办法,就是 使用 abstract 关键字,这样就完全不需要实现这个方法了.
public abstract String getDescription();
// no implementation required
    为了提高程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的.
abstract class Person
{
    ...
    public abstract String getDescription();
}
    除了抽象方法之外,抽象类还可以包含具体数据和具体方法.例如,Person类还保存着姓名和一个返回姓名的具体方法.
abstract class Person
{
    private String name;
    public Person(String n)
    {
        name = n;
    }
    public abstract String getDescription();
    public String getName()
    {
        return name;
    }
}
    提示:许多程序员认为,在抽象类中不能包含具体方法.建议尽量将通用的域或方法(不管是否是抽象的)放在超类(不管是否是抽象类)中.
    抽象方法充当着占位的角色,它们的具体实现在子类中.扩展抽象类可以有两种选择.一种是在子类中定义部分抽象方法或不定义抽象方法,这样就必须将子类也标记为抽象类.另一种是定义全部的抽象方法,这样子类就不是抽象的了.
    类即使不含抽象方法,也可以将类声明为抽象类.
    抽象类不能被实例化,也就是说,如果将一个类声明为 abstract,就不能创建这个类的对象.例如,表达式
new Person("Vince Vu")
    是错误的,但可以创建一个具体子类的抽象.
    需要注意,可以定义一个抽象类的对象变量,但是它只能引用非抽象类的对象.例如,
Person p = new Student("Vince vu", "Economics");
    这里的p是一个抽象类Person的变量,Person引用了一个非抽象子类Student的实例.
    注释:在C++中,有一种在尾部=0标记的抽象方法,称为纯虚函数,例如:
class Person        // C++
{
public:
    virtual string getDescription() = 0;
    ...
};  
    只要有一个纯虚函数,这个类就是抽象类.在C++中,没有提供用于表示抽象类的特殊关键字.
    下面通过抽象类Person扩展一个具体子类Student:
class Student extends Person
{
    private String major;
    public Student(String n, String m)
    {
        super(n);
        major = m;
    }
    public String getDescription()
    {
        return "a student majoring in " + major;
    }
}
    在Student类中定义了getDescription方法.因此,在Student类中的全部方法都是非抽象的,这个类不再是抽象类.
    程序5-4中演示抽象超类Person对象调用抽象方法.
    abstractClasses/PersonTest.java如下所示:
package abstractClasses;

public class PersonTest
{
   public static void main(String[] args)
   {
       Person[] people = new Person[2];
       
       // fill the people array with Student and Employee objects
       people[0] = new Employee("harry", 4000, 1999, 1, 1);
       people[1] = new Student("maria", "computer science");


       // print out names and description of all Person objects
       for (Person p : people)
           System.out.println(p.getName() + ", " + p.getDescription());
   }
}
    Person.java如下所示:
package abstractClasses;

public abstract class Person
{
    public abstract String getDescription();
    private String name;

    public Person(String n)
    {
        name = n;
    }
    public String getName()
    {
        return name;
    }
}
    Employee.java如下所示:
package abstractClasses;

import java.util.Date;
import java.util.GregorianCalendar;

public class Employee extends Person
{
    private double salary;
    private Date hireDay;

    public Employee(String n, double s, int year, int month, int day)
    {
        super(n);
        salary = s;
        GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
        hireDay = calendar.getTime();
    }
    public double getSalary()
    {
        return salary;
    }
    public Date getHireDay()
    {
        return hireDay;
    }
    public String getDescription()
    {
        return String.format("an employee with a salary of $%.2f", salary);
    }
    public void raiseSalary(double byPercent)
    {
        double raise = salary * byPercent / 100;
        salary += raise;
    }
}
    Student.java如下所示:
package abstractClasses;

public class Student extends Person
{
    private String major;

    /**
     * @param n the student's name
     * @param m the student's major
     */
    public Student(String n, String m)
    {
        // pass n to superclass constructor
        super(n);
        major = m;
    }
    public String getDescription()
    {
        return "a student majoring in " + major;
    }
}
    运行结果如下所示:

5.1.7   受保护访问

    一般而言,最好将类中的域标记为 private,而方法标记为 public . 任何声明为 private 的内容对其他类都是不可见的.这对于子类来说也是完全适用的,即子类也不能访问超类的私有域.
    然而,在有些时候, 希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域.为此,需要将这些方法或域声明为 protected .例如,如果将超类Employee中的hireDay声明为 protected,而不是私有的,Manager中的方法就可以直接地访问它.
    不过,Manager方法只能够访问Manager对象中的hireDay域,而不能访问其他Employee对象中的这个域. 这种限制有助于避免滥用受保护机制,使得子类只能获得访问受保护域的权利.
    受保护的方法更具有实际意义,如果需要限制某个方法的使用,就可以将它声明为 protected,这表明子类得到信任,可以正确地使用这个方法,而其他类不行.

    这个方法的一个最好的示例就是Object类中的clone方法.
    注释:事实上,Java中的受保护部分对所有子类及同一个包中的其他类都可见,这与C++中的受保护机制稍有不同,Java中的 protected 概念要比C++中的安全性差.
    下面归纳一下Java用于控制可见性的4个访问修饰符:
    1.仅对本类可见:private
    2.对所有类可见:public
    3.对本包和所有子类可见:protected
    4.对本包可见:默认,不需要修饰符


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值