<Core Java> 5.1 类、超类和子类

声明:这章的内容可能会引用上章的有关例子,使用例子时会直接引用不再做过多描述,有不懂的地方请查阅之前的学习笔记。
之前有构造一个雇员Employee类,实际公司雇员中还包含经理等一些其他特殊职位的雇员,他们的一些情况信息会有所不同。例如薪金的算法。假设雇员只有固定的基本薪金,而经理有相应的奖金制度。这样二者绝大多数情况一样仅存在少数的不同,若要为经理重新创建一个新类显然是不实际的。为了解决这样的问题就引入了本章的内容——继承。

1、继承用关键字extends表示正在构造的新类派生于已存在的类,写法如下:
public Manager extends Employee {
    //添加域和方法
}
已存在的类被称为超类(superclass)、基类(baseclass)或父类(parentclass);新类被称为子类(subcalss)、派生类(derivedclass)或孩子类(childclass)

在Java中所有的继承都是公有继承(所谓的公有继承就是不改变超类成员在子类中的访问权限。即超类中的public成员,private成员或protect成员在子类中仍为原有访问权限)

2、在Manager类中增加一个存放奖金的域并且设置一个操作这个域的方法:
class Manager extends Empolyee {
    ...
    public void setBonus(double aBonuds) {
        bonus = aBonuds;
    }
    private double bonus;
}
如果有个Manager对象就可以使用setBouns方法:
Manager boss = new Manager(...);
boss.setBonus(5000);

3、因为setBouns方法不是在Employee类中定义的,所以Employee类不能使用此方法。但是在Manager类中虽然没有显式地定义getName和getHireDay方法但是属于Manager类的对象可以使用它们,因为Manager类自动继承了Employee类的方法。当然除了继承方法还继承了超类的所有域。所以在扩展子类时只需指明与超类的不同之处即可。
因此,在设计类时通用类应该放在超类中,特殊类放在相应的子类中。避免重复书写代码。

4、在超类中有些方法并不完全适用与子。例如,Manager类中的getSalary方法需要返回薪金和奖金总和。这就需要提供一个新的方法来重写(override)超类中的方法:(注意重写的方法仅在相应的子类中有效,不会改变超类中的方法)
calss Manager extends Employee {
    ...
    public double getSalary() {
        ...
    }
}问题是如何实现这个方法呢?

1)只要返回salary和bonus域的总和就行了。(实际是不可行的)
public doublie getSalary() {
    return salary + bonus;    //不可行
}
因为Manager类的getSalary方法不可以直接访问超类的私有域。

2)试图调用Employee类的getSalary方法(想法是对的,但是下面操作有误)
public double getSalary() {
    double baseSalary = getSalary;    //不可行
    return baseSalary + bonus;
}
因为Manager类中也有一个getSalary方法(就是正在实现的方法),这样书写会导致程序无限制调用自己直至程序崩溃!

3)我们想调用的是超类中的getSalary方法,而不是当前类中的同名方法,就要通过关键字super实现:
public double getSalary() {
    double baseSalary = super.getSalary();
    return baseSalary + bonus;
}

5、关键字this和super的异同:
1)super与this的引用是不同概念,super不是一个对象的引用,不能将super赋给一个对象变量,super只是一个指示编译器调用超类方法的关键字。

2)this有两个用途:(1)引用隐式参数;(2)调用该类的其他构造器。
super也是两个用途:(1)调用超类方法;(2)调用超类构造器。
二者使用方式一致:调用另一个构造器的语句必须放在这个构造器的第一句代码中。构造参数即可以传递给本类(this)的其他构造器,也可以传递给超类(super)的构造器。

6、子类可以增加域、方法或重写方法,但是绝对不能删除继承的任何域和方法!!!

7、用super关键字调用超类中的构造器:
public Manager(String aName, double aSalary, int year, int month, int day) {
    super(aName, aSalary, year, month, day);
    bonus = 0;
}
语句super(aName, aSalary, year, month, day);是调用超类Employee中含有aName, aSalary, year, month, day参数的构造器。

因为子类中的构造器不能访问超类中的私有域,所以用super调用超类中的构造器对这部分数据初始化。

8、源代码:例5-1,展示Employee对象与Manager对象在计算薪金上的区别:
ManagerTest.java:
  1. package com.vincent.corejava.managertest;  
  2.   
  3. public class ManagerTest {  
  4.     public static void main(String[] args) {  
  5.         Manager boss = new Manager("vincent"100001991715);  
  6.         boss.setBonus(8000);  
  7.       
  8.       
  9.     Employee[] staff = new Employee[3];  
  10.     staff[0] = boss;  
  11.     staff[1] = new Employee("huangchang"50001991101);  
  12.     staff[2] = new Employee("wulin"6000199188);  
  13.       
  14.     for (Employee e : staff)  
  15.         System.out.println("name: " + e.getName()  
  16.                 + ", salary: " + e.getSalary());  
  17.     }  
  18. }  

Manager.java:
  1. package com.vincent.corejava.managertest;  
  2.   
  3. public class Manager extends Employee {  
  4.   
  5.     private double bonus;  
  6.   
  7.     public Manager(String aName, double aSalary, int year, int month, int day) {  
  8.         super(aName, aSalary, year, month, day);  
  9.         bonus = 0;  
  10.     }  
  11.       
  12.     public double getSalary() {  
  13.         double baseSalary = super.getSalary();  
  14.         return baseSalary + bonus;  
  15.     }  
  16.       
  17.     public void setBonus(double aBonus) {  
  18.         bonus = aBonus;  
  19.     }  
  20.   
  21. }  

Employee.java:
  1. package com.vincent.corejava.managertest;  
  2.   
  3. import java.util.Date;  
  4. import java.util.GregorianCalendar;  
  5.   
  6. public class Employee {  
  7.     private String name;  
  8.     private double salary;  
  9.     private Date hireDay;  
  10.   
  11.     public Employee(String aName, double aSalary, int year, int month, int day) {  
  12.         name = aName;  
  13.         salary = aSalary;  
  14.         GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);  
  15.         hireDay = calendar.getTime();  
  16.     }  
  17.       
  18.     public String getName() {  
  19.         return name;  
  20.     }  
  21.       
  22.     public double getSalary() {  
  23.         return salary;  
  24.     }  
  25.       
  26.     public Date getHireDay() {  
  27.         return hireDay;  
  28.     }  
  29.       
  30.     public void raiseSalary (double byPercent) {  
  31.         double raise = salary * byPercent / 100;  
  32.         salary += raise;  
  33.     }  
  34. }  


运行结果:
name: vincent, salary: 18000.0
name: huangchang, salary: 5000.0
name: wulin, salary: 6000.0

注意:代码中e.getSalary()能够确定执行哪个getSalary方法。虽然e被声明在Employee类中,实际e可以引用Employee对象也可以引用Manager对象。一个对象变量(例如变量e)可以引用多种实际类型的现象被称为多态。在运行中自动选择调用哪个方法的现象称为动态绑定。

5.1.3 动态绑定
弄清调用过程是很重要的,下面是调用的详细过程:
1)编译器查看对象的声明类型和方法名。此时,编译器获取了所有可能被调用的候选方法。
2)编译器查看调用方法时提供的参数类型。此时,编译器获取了需要调用的方法名和参数类型。
3)如果是private、static、final方法或者构造器,编译器可以很准确的知道调用哪个方法,这种调用方式称为静态绑定。
对应的是,依赖于隐式参数的实际类型,并在运行过程中实现动态绑定。
4)当程序运行并采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类最合适的那个类方法。
像这样每次调用方法都要进行搜索,时间开销很大,所以JVM预先为每个类创建好了方法表,表中列出了所有方法的签名和实际调用方法。这样需要调用方法时只需查找这个表就行了。

在运行时,调用e.getSalary()方法的过程:
1)JVM提取e的实际类型的方法。
2)JVM搜索定义getSalary签名的类。此时JVM已经知道该调用哪个方法了。
3)JVM调用方法。

5.1.4 组织继承:final类和方法
不允许扩展的类称为final类。定义类时使用final修饰符表明这个类是final类。声明格式:
final class Executive extends Manager {
    ...
}

类中的方法也能被声明final。这样做就表明子类不能重写这个方法(final类中的所有方法自成为final方法)。例如:
class Employee {
    public final String getName() {
        return name;
    }
    ...
}
注释:域也可以声明为final。对final域来说,构造对象之后就不允许改变他的值。如果将一个类声明为final类,只是他的方法会自动变成final方法,域不会改变。

5.1.6 抽象类
1、祖先类更为通用和抽象,它往往用来作为派生其他类的超类,而不作为要实现特定的实例类。抽象类使关键字abstract修饰:
abstract class Person {
    ....
}

2、包含一个以上抽象方法的类要将类声明为抽象类。当然,抽象类除了包含抽象方法外,还可以有具体数据和具体方法。例如:Person类还保存着姓名和返回姓名的方法:
abstract class Person {
    public Person(String aName) {
        name = aName;
    }
    public abstract String getDescription();
    public String getName() {
        return name;
    }
    private name;
}
提示:很多程序员认为抽象类中不能包含具体方法。建议尽量将通用的域和方法(不管是不是抽象的)放在超类中。
类即使不含抽象方法也可以定义为抽象类。抽象类不能被实例化(即一个被声明为abstract的类不能new出相应的对象)。

3、源代码:例5-2中定义了抽象超类Person和两个具体子类Employee和Student。
PersonTest.java:
  1. package com.vincent.corejava.persontest;  
  2.   
  3. public class PersonTest {  
  4.     public static void main(String[] args) {  
  5.         Person[] people = new Person[2];  
  6.           
  7.         people[0] = new Employee("Harry"50001989101);  
  8.         people[1] = new Student("Vincent""computer science");  
  9.           
  10.         for (Person p : people)  
  11.             System.out.println(p.getName() + "," + p.getDescription());  
  12.     }  
  13.   
  14. }  

Person.java:
  1. package com.vincent.corejava.persontest;  
  2.   
  3. abstract class Person {  
  4.     private String name;  
  5.   
  6.     public Person(String aName) {  
  7.         name = aName;  
  8.     }  
  9.       
  10.     public abstract String getDescription();  
  11.       
  12.     public String getName() {  
  13.         return name;  
  14.     }  
  15. }  

Employee.java:
  1. package com.vincent.corejava.persontest;  
  2.   
  3. import java.util.Date;  
  4. import java.util.GregorianCalendar;  
  5.   
  6. public class Employee extends Person {  
  7.   
  8.     private double salary;  
  9.     private Date hireDay;  
  10.   
  11.     public Employee(String aName, double aSalary, int year, int month, int day) {  
  12.         super(aName);  
  13.         salary = aSalary;  
  14.         GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);  
  15.         hireDay = calendar.getTime();  
  16.     }  
  17.       
  18.     public double getSalary() {  
  19.         return salary;  
  20.     }  
  21.       
  22.     public Date getHireDay() {  
  23.         return hireDay;  
  24.     }  
  25.   
  26.     @Override  
  27.     public String getDescription() {  
  28.         return String.format("an employee with a salary of $%.2f", salary);  
  29.     }  
  30.       
  31.     public void raiseSalary(double byPercent) {  
  32.         double raise = salary * byPercent / 100;  
  33.         salary += raise;  
  34.     }   
  35.   
  36. }  

Student.java:
  1. package com.vincent.corejava.persontest;  
  2.   
  3. public class Student extends Person {  
  4.     private String major;  
  5.   
  6.     public Student(String aName, String aMajor) {  
  7.         super(aName);  
  8.         major = aMajor;  
  9.     }  
  10.   
  11.     @Override  
  12.     public String getDescription() {  
  13.         return "a student majoring in " + major;  
  14.     }  
  15. }  


运行结果:
Harry,an employee with a salary of $5000.00
Vincent,a student majoring in computer science

5.1.7 受保护的访问
1、建议将域标记为private,将方法标记为public。如果希望超类中的某些方法被子类访问,或允许子类的方法访问超类的某个域,就需要将这些方法或域标记为protected。注意:要谨慎使用protected属性,滥用可能导致破坏数据封装原则!!!
相比而言将方法标记为protecetd要实际的多,如果要限制某个方法的使用,就可以将它声明为protected

2、归纳下Java中用于控制访问权限的修饰符:
1)private——仅本类可访问
2)public——所有类均可访问
3)protected——本包和所有子类可访问
4)默认(即没有任何修饰符标识)——本包可访问
posted @ 2013-03-01 18:31  vincent_hv  阅读(675)  评论(0编辑  收藏  举报