[关闭]
@wjcper2008 2017-05-16T13:26:41.000000Z 字数 26764 阅读 3100

面向对象部分2: 继承和多态

Java


参考自《Java语言程序设计 基础篇 10版》, 仅供内部教学讲义使用, 由Teacher Chen整理.

1 类的抽象和封装

知识点:

  1. 类的抽象是指将类的实现和类的使用分离开, 实现的细节被封装并且对使用者隐藏.
  2. 类抽象(class abstraction): 将类的实现和使用分离.

类的设计者只需描述类的功能, 让使用者明白如何才能使用类. 而类的使用者不需要知道类是如何实现的.

类的封装(class encapsulation): 实现的细节经过封装, 对用户隐藏起来.

类的合约(class's contract): 类的设计者描述使用者如何访问数据域和使用类的方法, 及其各方法的具体功能.
image_1bed3b7elf5rg2p37j50u1n3j9.png-71.8kB

类也称为抽象数据类型(Abstract Data Type, ADT).

例如: 可以创建一个Circle对象, 并且可以在不知道面积是如何计算出来的情况下, 求出这个圆的面积.

image_1bed3hvi06t9au811sc14hj1q9jm.png-62.3kB

现实生活中的许多例子都可以说明类抽象的概念.

例如: 考虑建立一个计算机系统. 个人计算机有很多组件: CPU, 内存, 磁盘, 主板和风扇等. 每个组件都可以看作是一个有属性和方法的对象. 要使各个组件一起工作, 只需要知道每个组件是怎么用的以及是如何与其他组件进行交互的, 而无须了解这些组件内部是如何工作的. 内部功能的实现被封装起来, 对用户是隐藏的. 所以, 用户可以组装一台计算机, 而不需要了解每个组件的功能是如何实现的.

1.2 案例: 计算贷款支付

贷款可以是车辆贷, 学生贷款, 或者一个住宅贷款.

1. 需求规范
程序必须满足以下要求:

2. 算法分析
输出是月支付额度总支付额度, 可以通过下面的公式进行计算:
image_1bed5uclk1q641pt71eho1mho13do13.png-53.8kB

因此, 程序需要的输入是月利率, 贷款的年数, 以及贷款额度.

1.2.1 面向过程的实现方式

系统设计阶段, 你确定程序中的以下步骤:

  1. 提示用户输人年利率, 年数以及贷款额度. (利率通常表示为对1年时间的本金的百分比, 这被称为年利率)
  2. 对于年利率的输入是一个百分比格式的数字, 比如4.5%. 程序需要通过除以100将它转换成为一个十进制数. 为了从年利率值得到月利率, 将它除以12, 因为1年有12个月. 因此, 为了得到十进制格式的月利率值, 需要将百分比格式的年利率数除以1200. 比如, 如果年利率是4.5%, 那么月利率则为4.5/1200 = 0.00375.
  3. 使用前面的公式计算月支付额度.
  4. 计算总支付额度, 等于月支付额度乘以12, 再乘以年数.
  5. 显示月支付额度和总共支付额度.

代码实现如下, 在公式中, 你需要计算总额, 可以通过使用Math.pow(1 + monthlyInterestRate, numberOfYears * 12). 得到:

代码清单: ComputeLoan.java

  1. import java.util.Scanner;
  2. public class ComputeLoan {
  3. public static void main(String[] args) {
  4. // Create a Scanner
  5. Scanner input = new Scanner(System.in);
  6. // Enter annual interest rate in percentage, e.g., 7.25%
  7. System.out.print("Enter annual interest rate, e.g., 7.25%: ");
  8. double annualInterestRate = input.nextDouble();
  9. // Obtain monthly interest rate
  10. double monthlyInterestRate = annualInterestRate / 1200;
  11. // Enter number of years
  12. System.out.print("Enter number of years as an integer, e.g., 5: ");
  13. int numberOfYears = input.nextInt();
  14. // Enter loan amount
  15. System.out.print("Enter loan amount, e.g., 120000.95: ");
  16. double loanAmount = input.nextDouble();
  17. // Calculate payment
  18. double monthlyPayment = loanAmount * monthlyInterestRate / (1 - 1 / Math.pow(1 + monthlyInterestRate, numberOfYears * 12));
  19. double totalPayment = monthlyPayment * numberOfYears * 12;
  20. // Display results
  21. System.out.println("The monthly payment is $" + (int)(monthlyPayment * 100) / 100.0);
  22. System.out.println("The total payment is $" + (int)(totalPayment * 100) / 100.0);
  23. }
  24. }

image_1bedes1h8107g9vhco71ofu1osrm.png-64.5kB

程序解析:


1.2.2 面向对象实现方式

为何要使用面向对象设计模式, 它给带来了什么优势:

  • 上面的贷款程序并不能在其他程序中直接被重用, 因为计算支付的代码放在main方法中. 解决这个问题的一种方式就是定义计算月偿还额和总偿还额的静态全局方法.
  • 但是, 这个解决方案是有局限性的. 假设希望将一个日期和这个贷款联系起来. 由于, 传统的面向过程式编程是动作驱动的, 数据和操作是分离的. 这功能能难实现.
  • 面向对象编程的模式重点在于对象, 数据和操作一起定义在对象中. 为了将日期和贷款联系起来, 可以定义一个贷款类, 将日期和贷款的其他属性一起作为数据域, 并且贷款数据和动作在一个类中集成设计.

接下来, 根据用户需求, 给出Load类的UML图设计.

image_1bedlas3ak18qub6meqk10je13.png-245.8kB

UML图看作Loan类的合约. 面向对象设计过程中, 你将扮演两个角色: 一个是类的用户, 一个是类的开发者. 记住用户可以在不知道类是如何实现的情况下使用类.

根据UML类图, 设计相应的Load类Java实现.
程序清单: Loan.java

  1. public class Loan {
  2. private double annualInterestRate;
  3. private int numberOfYears;
  4. private double loanAmount;
  5. private java.util.Date loanDate;
  6. /** Default constructor */
  7. public Loan() {
  8. this(2.5, 1, 1000);
  9. }
  10. /** Construct a loan with specified annual interest rate,
  11. number of years and loan amount
  12. */
  13. public Loan(double annualInterestRate, int numberOfYears,
  14. double loanAmount) {
  15. this.annualInterestRate = annualInterestRate;
  16. this.numberOfYears = numberOfYears;
  17. this.loanAmount = loanAmount;
  18. loanDate = new java.util.Date();
  19. }
  20. /** Return annualInterestRate */
  21. public double getAnnualInterestRate() {
  22. return annualInterestRate;
  23. }
  24. /** Set a new annualInterestRate */
  25. public void setAnnualInterestRate(double annualInterestRate) {
  26. this.annualInterestRate = annualInterestRate;
  27. }
  28. /** Return numberOfYears */
  29. public int getNumberOfYears() {
  30. return numberOfYears;
  31. }
  32. /** Set a new numberOfYears */
  33. public void setNumberOfYears(int numberOfYears) {
  34. this.numberOfYears = numberOfYears;
  35. }
  36. /** Return loanAmount */
  37. public double getLoanAmount() {
  38. return loanAmount;
  39. }
  40. /** Set a newloanAmount */
  41. public void setLoanAmount(double loanAmount) {
  42. this.loanAmount = loanAmount;
  43. }
  44. /** Find monthly payment */
  45. public double getMonthlyPayment() {
  46. double monthlyInterestRate = annualInterestRate / 1200;
  47. double monthlyPayment = loanAmount * monthlyInterestRate / (1 -
  48. (Math.pow(1 / (1 + monthlyInterestRate), numberOfYears * 12)));
  49. return monthlyPayment;
  50. }
  51. /** Find total payment */
  52. public double getTotalPayment() {
  53. double totalPayment = getMonthlyPayment() * numberOfYears * 12;
  54. return totalPayment;
  55. }
  56. /** Return loan date */
  57. public java.util.Date getLoanDate() {
  58. return loanDate;
  59. }
  60. }

作为Loan类的使用者, 设计测试类. main方法读取利率和还贷时间(以年为单位)以及贷款总额, 创建一个 Loan对象, 然后使用Loan类中的实例方法获取月偿还额(第29行)和总偿还额(第30行).

代码清单: TestLoanClass.java

  1. import java.util.Scanner;
  2. public class TestLoanClass {
  3. /** Main method */
  4. public static void main(String[] args) {
  5. // Create a Scanner
  6. Scanner input = new Scanner(System.in);
  7. // Enter yearly interest rate
  8. System.out.print("Enter yearly interest rate, for example, 8.25: ");
  9. double annualInterestRate = input.nextDouble();
  10. // Enter number of years
  11. System.out.print("Enter number of years as an integer: ");
  12. int numberOfYears = input.nextInt();
  13. // Enter loan amount
  14. System.out.print("Enter loan amount, for example, 120000.95: ");
  15. double loanAmount = input.nextDouble();
  16. // Create Loan object
  17. Loan loan = new Loan(annualInterestRate, numberOfYears, loanAmount);
  18. // Display loan date, monthly payment, and total payment
  19. System.out.printf("The loan was created on %s\n" + "The monthly payment is %.2f\nThe total payment is %.2f\n", loan.getLoanDate().toString(), loan.getMonthlyPayment(), loan.getTotalPayment());
  20. }
  21. }

image_1bedmfq461m2hd5298vuuciic1g.png-63.7kB

1.3 面向对象设计的优势

由上述实例可以看出, 面向对象程序设计方式这有三个优点:

  1. 揭示了开发类和使用类是两个不同的任务.
  2. 能使你跳过某个类的复杂实现, 将任务分解为小功能去实现, 而不打乱整体的顺序.
  3. 扩展能力强, 数据和操作绑定.

2 类的关系

知识点: 为了设计类, 需要探究类之间的关系. 类中间的关系通常是关联, 聚合, 组合以及继承.

2.1 关联

关联是一种常见的二元关系, 描述两个类之间的活动.

学生选取课程是Student类和Course类之间的一种关联, 而教师教授课程是Faculty类和Course类之间的关联. 这些关联可以使用UML图形标识来表达.
image_1bedo32jfdo01gl81qdu1nsu1k4v1t.png-24.1kB
图 该UML图显示学生可以选择任意数童的课程, 教师最多可以教授3门课程, 每门课程可以有5到60个学生, 并且每门课程只由一位教师来教授.

UML图中的关联由两个类之间的实线表示, 可以有一个可选的标签描述关系, 图中标签是Take和Teach. 每个关系可以有一个可选的小的黑色三角形表明关系的方向. 在该图中, 方向表明学生选取课程(而不是相反方向的课程选取学生).

关系中涉及的每个类可以有一个角色名称, 描述在该关系中担当的角色. Teacher是Faculty的角色名.

2.1.1 关联的多重性

关联中涉及的每个类可以给定一个多重性(multiplicity), 放置在类的边上用于给定UML图中关系所涉及的类的对象数.

多重性可以是一个数字或者一个区间, 决定在关系中涉及类的多少个对象. 字符*意味着无数多个对象, 而m..n表示对象数处于m和n之间, 并且包括m和n.

比如, 每个学生可以选取任意数量的课程数, 每门课程可以有至少5个最多60个学生. 每门课程只由一位教师教授, 并且每位教师每学期可以教授0到3门课程.

2.1.2 关联关系的实现

可以通过使用数据域以及方法来实现关联.

image_1bedpg3nf9cq1cnc1ehd1h7t1fe2a.png-86kB
图 关联关系使用类中的数据域和方法来实现

实现类之间的关系可以有很多种可能的方法. 例如, Course类中的学生和教师信息可以省略, 因为它们已经在Student和Faculty类中了. 同样的, 如果不需要知道一个学生选取的课程或者教师教授的课程, Student 或者Faculty类中的数据域courseList和addCourse方法也可以省略.

2.2 聚集和组合

知识点: 聚集是关联的一种特殊形式, 代表了两个对象之间的归属关系. 聚集UML图中为has-a关系.

所有者对象称为聚集对象, 它的类称为聚集类. 而从属对象称为被聚集对象, 它的类称为被聚集类. 一个对象可以被多个其他的聚集对象所拥有.

知识点: 如果一个对象只归属于一个聚集对象, 那么它和聚集对象之间的关系就称为组合(composition). 组合是一种特殊的聚集方式.

例如: "一个学生有一个名字" 就是学生类 Student与名字类Name之间的一个组合关系, 而 "一个学生有一个地址"是学生类Student与地址类Address之间的一个聚集关系, 因为一个地址可以被几个学生所共享.

image_1bedqde4thgtclrd416jcaa92n.png-30.7kB

在UML中, 附加在聚集类(Student)上的实心菱形表示它和被聚集类(Name)之间具有组合关系; 而附加在聚集类(Student)上的空心菱形表示它与被聚集类(Address)之间具有聚集关系.

3. 类的继承(重要)

3.1 继承的必要性

知识点:

在面向对象编程中, 允许类的设计者基于现有的类去扩展新的类, 即站在巨人肩膀上设计类, 这种类的设计模式称之为继承.

继承是Java在代码复用方面一个重要且功能强大的特征. 例如,

  1. 假设要设计三个类, 分别用于描述圆, 矩形 和 三角形. 观察发现, 圆类, 矩形类 和 三角形类 具有很多共有的特征, 都是一种几何形状.
  2. 如果分别独立的设计这三个类, 势必会造成代码冗余.
  3. 那么有没有种更易于理解和维护的类的设计模式呢? 答案就是继承.

3.2 父类和子类

继承的基本步骤:

  1. 首先, 设计一个具有通用性功能的类(父类).
  2. 然后, 依据需求, 扩充该通用类(父类)的功能, 设计功能更细化或强大的类(子类).

例如, 我们需要设计一堆类(如圆类, 矩形类, 三角形类), 而这些不同类之间可能存在某些共同的性质和功能. 因此 我们可为这些共同的性质和功能设计一个通用的父类(几何类). 通用类(几何类)可以被这些类(如圆类, 矩形类, 三角形类)所共享.

然后, 我们继承通用类, 进一步设计具有特殊性质和功能的子类(如圆类, 矩形类, 三角形类).

3.3 案例分析: 几何对象

3.3.1 UML类图

考虑一下几何对象. 假设要设计类, 来描述像圆和矩形这样的几何对象. 几何对象有许多共同的属性和行为. 它们可以是用某种颜色画出来的 , 填充的或者不填充的.

这样, 一个通用类 GeometricObject 可以用来描述所有的几何对象. 这个类的数据域包括 颜色color属性 和 是否填充filled属性, 以及适用于这些数据域的 get 和 set 方法. (回忆下封装)

假设该通用类还包括 数据域 创造时间dateCreated 以及其getDateCreated()方法.

最后, 该类还包括一个toString()方法, 用于返回代表该对象的字符串. 下面为GeometricObject类的UML类图.

image_1bet8o26l17p81nth1ti41r9fjivm.png-163.5kB

由于圆是一个特殊类型的几何对象, 所以它和其他几何对象共享共同的属性和方法. 因此, 可通过继承自 GeometricObject类来定义Circle类. 同理, Rectangle类也可以定义为 GeometricObject 的子类.

在UML类图中, 为显示这些类之间的继承关系, 这里使用指向父类的三角箭头用来表示相关的两个类之间的继承关系. 这里GeometricObject类为父类, 而Circle类和Rectangle类为子类.

image_1bet5drsm7chqhn1v1h9lk7lp9.png-296.3kB

知识点:

  1. 在Java术语中, 如果类C1扩展自另一个类C2, 那么就将C1称为次类(subclass), 将C2称为超类(superclass).
  2. 超类也称为父类(parent class)或基类(base class)
  3. 次类又称为子类(child class), 扩展类(extended class) 或派生类(derived class).
  4. 子类从它的父类中继承可访问的数据域和方法, 还可以添加新数据域和新方法.

3.3.2 源代码

GeometricObject类, Circle类和Rectangle类分别在程序清单实现.

代码清单 SimpleGeometricObject.java

  1. public class SimpleGeometricObject {
  2. private String color = "white";
  3. private boolean filled;
  4. private java.util.Date dateCreated;
  5. /** Construct a default geometric object */
  6. public SimpleGeometricObject() {
  7. dateCreated = new java.util.Date();
  8. }
  9. /** Construct a geometric object with the specified color
  10. * and filled value */
  11. public SimpleGeometricObject(String color, boolean filled) {
  12. dateCreated = new java.util.Date();
  13. this.color = color;
  14. this.filled = filled;
  15. }
  16. /** Return color */
  17. public String getColor() {
  18. return color;
  19. }
  20. /** Set a new color */
  21. public void setColor(String color) {
  22. this.color = color;
  23. }
  24. /** Return filled. Since filled is boolean,
  25. its get method is named isFilled */
  26. public boolean isFilled() {
  27. return filled;
  28. }
  29. /** Set a new filled */
  30. public void setFilled(boolean filled) {
  31. this.filled = filled;
  32. }
  33. /** Get dateCreated */
  34. public java.util.Date getDateCreated() {
  35. return dateCreated;
  36. }
  37. /** Return a string representation of this object */
  38. public String toString() {
  39. return "created on " + dateCreated + "\ncolor: " + color +
  40. " and filled: " + filled;
  41. }
  42. }

代码清单 CircleFromSimpleGeometricObject.java

  1. public class CircleFromSimpleGeometricObject
  2. extends SimpleGeometricObject {
  3. private double radius;
  4. public CircleFromSimpleGeometricObject() {
  5. }
  6. public CircleFromSimpleGeometricObject(double radius) {
  7. this.radius = radius;
  8. }
  9. public CircleFromSimpleGeometricObject(double radius,
  10. String color, boolean filled) {
  11. this.radius = radius;
  12. setColor(color);
  13. setFilled(filled);
  14. }
  15. /** Return radius */
  16. public double getRadius() {
  17. return radius;
  18. }
  19. /** Set a new radius */
  20. public void setRadius(double radius) {
  21. this.radius = radius;
  22. }
  23. /** Return area */
  24. public double getArea() {
  25. return radius * radius * Math.PI;
  26. }
  27. /** Return diameter */
  28. public double getDiameter() {
  29. return 2 * radius;
  30. }
  31. /** Return perimeter */
  32. public double getPerimeter() {
  33. return 2 * radius * Math.PI;
  34. }
  35. /* Print the circle info */
  36. public void printCircle() {
  37. System.out.println("The circle is created " + getDateCreated() +
  38. " and the radius is " + radius);
  39. }
  40. }

代码清单 RectangleFromSimpleGeometricObject.java

  1. public class RectangleFromSimpleGeometricObject
  2. extends SimpleGeometricObject {
  3. private double width;
  4. private double height;
  5. public RectangleFromSimpleGeometricObject() {
  6. }
  7. public RectangleFromSimpleGeometricObject(
  8. double width, double height) {
  9. this.width = width;
  10. this.height = height;
  11. }
  12. public RectangleFromSimpleGeometricObject(
  13. double width, double height, String color, boolean filled) {
  14. this.width = width;
  15. this.height = height;
  16. setColor(color);
  17. setFilled(filled);
  18. }
  19. /** Return width */
  20. public double getWidth() {
  21. return width;
  22. }
  23. /** Set a new width */
  24. public void setWidth(double width) {
  25. this.width = width;
  26. }
  27. /** Return height */
  28. public double getHeight() {
  29. return height;
  30. }
  31. /** Set a new height */
  32. public void setHeight(double height) {
  33. this.height = height;
  34. }
  35. /** Return area */
  36. public double getArea() {
  37. return width * height;
  38. }
  39. /** Return perimeter */
  40. public double getPerimeter() {
  41. return 2 * (width + height);
  42. }
  43. }

代码清单 TestCircleRectangle.java

  1. public class TestCircleRectangle {
  2. public static void main(String[] args) {
  3. CircleFromSimpleGeometricObject circle = new CircleFromSimpleGeometricObject(1);
  4. System.out.println("A circle " + circle.toString());
  5. System.out.println("The color is " + circle.getColor());
  6. System.out.println("The radius is " + circle.getRadius());
  7. System.out.println("The area is " + circle.getArea());
  8. System.out.println("The diameter is " + circle.getDiameter());
  9. RectangleFromSimpleGeometricObject rectangle = new RectangleFromSimpleGeometricObject(2, 4);
  10. System.out.println("\nA rectangle " + rectangle.toString());
  11. System.out.println("The area is " + rectangle.getArea());
  12. System.out.println("The perimeter is " +
  13. rectangle.getPerimeter());
  14. }
  15. }

image_1bf25rskmvvg1lbtkqm13df64d13.png-104kB

3.3.3 代码分析

Circle类 使用下面的语法扩展 GeometricObject类:
image_1bf253ntq1ieengj70t72ohcc9.png-30.2kB

关键字 extends 告诉编译器, Circle 类扩展自 GeometricObject类, 这样, 它就继承了 getColor, setColor, isFilled, setFilled 和 toString 方法.

重载的构造方法 Circle(double radius, String color, boolean filled) 是通过调用 setColor 和 setFilled 方法设置 color 和 filled 属性来执行的. 这两个公共方法是在基类 GeometricObject类 中定义的, 并在 Circle 中继承, 因此可以在 Circle类中使用它们.

知识点: 子类中不能直接访问父类的private数据域.

你可能会尝试在构造方法中使用数据域 color 和 filled, 如下所示:

  1. public CircleFromSimpleGeometricObject(double radius, String color, boolean filled) {
  2. this.radius = radius;
  3. this.color = color; // Illegal color是父类的私有数据域
  4. this.filled = filled; // Illegal filled是父类的私有数据域
  5. }

这是错误的, 因为 GeometricObject类 中的私有数据域 color 和 filled 是不能被除了GeometricObject类. 本身之外的其他任何类访问的. 唯一读取和改变 color 与 filled 的方法就是通过它们的 get 和 set 方法.

Rectangle类 使用下面的语法继承 GeometricObject类:
image_1bf25inf2esp1a8h18h01nlggl8m.png-32.4kB

关键字 extends 告诉编译器 Rectangle 类继承自 GeometricObject类, 也就是继承了 getColor, setColor, isFilled, setFilled 和 toString 等方法.

3.4 知识点总结

下面是关于继承应该注意的几个关键点:

3.5 思考题

4 使用 super 关键字

知识点:

  1. 关键字 super 指代父类, 可以用于调用父类中的普通方法和构造方法.

  2. 子类继承它的父类中所有可访问的数据域和方法. ps: private可以吗?

子类它能继承父类的构造方法吗? 父类的构造方法能够在子类中调用吗? 本节就来解决这些问题以及衍生出来的问题.

关键字 this 的作用, 它是对调用对象的引用. 关键字 super 是指该对象的父类引用.

关键字 super 可以用于两种途径:

  1. 调用父类的构造方法.
  2. 调用父类的方法.

4.1 调用父类的构造方法

构造方法用于构建一个类的实例. 不同于属性和普通方法, 父类的构造方法不会被子类继承. 它们只能使用关键字 super 从子类的构造方法中显示的被调用.

调用父类构造方法的语法是:
1. 调用父类的无参构造方法

super( ) 

或者
2. 调用与参数匹配的父类的有参构造方法

super(parameters);

语句 super( ) 和 super(arguments) 必须出现在子类构造方法的第一行, 这是显式调用父类构造方法的唯一方式.

例如, 在CircleFromSimpleGeometricObject.java程序清单中, 替换构造方法, 调用父类的构造方法.

  1. public CircleFromSimpleGeometricObject(double radius, String color, boolean filled) {
  2. super(color, filled);
  3. this.radius = radius;
  4. }

知识点:

要调用父类构造方法就必须使用关键字 super, 而且这个调用必须是构造方法的第1条语句. 在子类中其他地方调用父类构造方法的名字将会引起一个语法错误.

4.2 构造方法链

构造方法可以调用重载的构造方法或父类的构造方法. 如果它们都没有被显式地调用, 编译器就会自动地将 super( ) 作为构造方法的第一条语句.

image_1bfjpod0k152o201n656se1pu09.png-88.3kB

在任何情况下, 构造一个类的实例时, 将会调用沿着继承链的所有父类的构造方法. 当构造一个子类的对象时, 子类构造方法会在完成自己的任务之前, 首先调用它的父类的构造方法.

如果父类继承自其他类, 那么父类构造方法又会在完成自己的任务之前, 调用它自己的父类的构造方法. 这个过程持续到沿着这个继承体系结构的最后一个构造方法被调用为止, 这就是构造方法链 (constructor chaining).

  1. //Faculty继承Employee类
  2. public class Faculty extends Employee {
  3. public static void main(String[] args) {
  4. new Faculty();
  5. }
  6. public Faculty() {
  7. System.out.println("(4) Performs Faculty's tasks");
  8. }
  9. }
  10. //Employee继承Person类
  11. class Employee extends Person {
  12. public Employee() {
  13. this("(2) Invoke Employee's overloaded constructor");
  14. System.out.println("(3) Performs Employee's tasks ");
  15. }
  16. public Employee(String s) {
  17. System.out.println(s);
  18. }
  19. }
  20. //父类Person类
  21. class Person {
  22. public Person() {
  23. System.out.println("(1) Performs Person's tasks");
  24. }
  25. }

image_1bfk0uhnmpng50g6pio801uv89.png-49.2kB

该程序会产生上面的输出. 为什么呢? 我们讨论一下这个原因. new
Faculty() 调用 Faculty 的无参构造方法. 由于 Faculty 是 Employee 的子类, 所以, 在 Faculty 构造方法中的所有语句执行之前, 先调用 Employee 的无参构造方法. Employee 的无参构造方法调用 Employee 的第二个有参构造方法. 由于 Employee 是 Person 的子类, 所以, 在 Employee 的第二个有参构造方法中所有语句执行之前, 先调用 Person 的无参构造方法. 这个过程如下图所示:
image_1bfk13a9q1h8fgbprhl1qm81tlem.png-55.6kB

知识点:
子类的构造方法默认首先调用父类的无参构造方法. 即在子类构造方法前, 默认加入super( ).

注意, 如果要设计一个可以被继承的类, 最好提供一个无参构造方法以避免程序设计错误. 思考下面的代码:

  1. public class Apple extends Fruit {
  2. //默认调用父类无参构造方法
  3. }
  4. class Fruit {
  5. public Fruit(String name) {
  6. System.out.println("Fruit's constructor is invoked");
  7. }
  8. }

分析: 由于在 Apple 中没有显式定义的构造方法, 因此, Apple 的默认无参构造方法被隐式调用. 因为 Apple 是 Fruit 的子类, 所以 Apple 的默认构造方法会自动调用 Fruit 的无参构造方法. 然而, Fruit 没有无参构造方法, 因为 Fruit 显式地定义了构造方法. 因此, 程序不能被成功编译.

4.3 调用父类的方法

关键字 super 不仅可以引用父类的构造方法, 也可以引用父类的方法. 所用语法如下:

super.method(parameters);

可以如下改写 Circle 类中的 printCircle() 方法:

  1. public void printCircle() {
  2. System.out.println("The circle is created " + super.getDateCreated() + " and the radius is " + radius);
  3. }

在这种情况下, 没有必要在 getDateCreated( ) 前放置 super, 因为 getDateCreated( ) 是GeometricObject 类中的一个方法并被 Circle 类继承.

4.4 思考题

5 方法重写

知识点:

  1. 子类从父类中继承方法. 有时, 子类需要修改父类中定义的方法的实现, 这称作方法重写 (method overriding)
  2. 要重写一个方法, 需要在子类中使用和父类一样的签名以及一样的返回值类型来对该方法进行重新定义.

5 方法重写的使用

比如, GeometricObject 类中的 toString() 方法, 返回表示几何
对象的字符串. 这个方法可以在子类Circle类中被重写, 返回表示圆的字符串. 为了重写它, 在程序清单中加入下面的新方法

  1. public class CircleFromSimpleGeometricObject extends SimpleGeometricObject {
  2. // Other methods are omitted
  3. // Override the toString method defined in the superclass
  4. public String toString() {
  5. return super.toString() + "\nradius is " + radius;
  6. }
  7. }

toString() 方法在 GeometricObject 类中定义, 在 Circle 类中被修改. 在这两个类中定义的方法都可以在 Circle 类中被调用. 要在 Circle 类中调用定义在 GeometricObject 中的 toString( ) 方法, 需要使用 super.toString().

注意:

  • Circle 的子类能用语法 super.super.toString() 访问定义在 GeometricObject 中的 toString() 方法吗? 答案是不能, 这是一个语法错误.
  • 不能通过super.super.来调用 父类的父类的方法.

5.2 方法重写的注意点

5.3 思考题

6 多态 Polymorphism

知识点: 多态意味着父类类型的引用变量可以指向子类对象.

面向对象程序设计的三大支柱是封装, 继承和多态. 前面, 已经学习了封装和继承, 本节将与大家探讨多态.

6.1 子类型和父类型

首先, 定义两个有用的术语: 子类型和父类型. 一个类实际上定义了一种类型. 子类定义的类型称为子类型 (subtype), 而父类定义的类型称为父类型 (supertype).

例如, Circle 是 GeometricObject 的子类型, 而 GeometricObject 是 Circle 的父类型.

6.2 多态思想

继承关系使一个子类继承父类的特征, 并且附加一些新特征. 子类是它的父类的特殊化, 每个子类的实例都是其父类的实例, 但是反过来就不成立.

例如, 每个圆都是一个几何对象, 但并非每个几何对象都是圆. 因此, 总可以将子类的实例传给具有父类类型的参数或变量.

  1. public class PolymorphismDemo {
  2. /** Main method */
  3. public static void main(String[] args) {
  4. // Display circle and rectangle properties
  5. displayObject(new CircleFromSimpleGeometricObject(1, "red", false));
  6. displayObject(new RectangleFromSimpleGeometricObject(1, 1, "black", true));
  7. }
  8. /** Display geometric object properties */
  9. public static void displayObject(SimpleGeometricObject object) {
  10. System.out.println("Created on " + object.getDateCreated() + ". Color is " + object.getColor());
  11. }
  12. }

image_1bfk4lcn715ua1l0c1lao1th5oo41g.png-25.7kB

分析: 方法 displayObject( ) 具有 GeometricObject 类型的参数. 可以通过传递任何一个 GeometricObject 的实例或其子类 (例如: new CircleFromSimpleCeometricObject(1, " red", false) 和 new RectangleFromSimpleGeometricObject(1, 1, "black", false)) 来
调用 displayObject.

多态: 使用父类对象的地方都可以使用子类的对象. 这就是通常所说的多态. (polymorphism, 它源于希腊文字, 意思是“多种形式”)

简单来说, 多态意味着父类型的变量可以引用子类型的对象.

多态思想的理解: phone类为手机类的父类, 苹果手机iPhone类为phone类的子类, 三星手机Samsung类也为phone类的子类. 当使用iphone发短信的时候, 可以说(调用)使用手机(phone类)来发短信, 它等价于使用iphone类来发短信. 同理, 使用Samsung发短信, 可以说(调用)使用手机(phone类)来发短信, 等价于使用Samsung类发短信.

这里的 phone类 为父类, 可以通过使用父类的变量来间接的调用子类某个功能.

7 多态的表现: 动态绑定

知识点: 方法可以通过重写在沿着继承链的多个父子类中实现. Java虚拟机决定运行时调用哪个方法.

方法可以在父类中定义而在子类中重写. 例如: toString( ) 方法是在 Object类中定义的, 而在 GeometricObject 类中重写toString( )方法. 思考下面的代码.

  1. Object o = new GeometricObject();
  2. System.out.println(o.toString());

7.1 声明类型和实际类型

这里的 o 调用哪个 tostring() 呢? 调用的是Object类的tostring()吗? 为了回答这个问题, 首先介绍两个术语: 声明类型和实际类型.

声明类型和实际类型:

  1. 变量在使用前, 必须要先声明, 而声明这个变量的类型被称之为 声明类型 (declared type).
    • 比如上面的 o 的声明类型为 Object 类.
  2. 实例(内存里对应的具体对象), 可以使用声明类型它的子类型的构造方法创建. 而, 该实例或变量所对应的真实类型被称为实际类型 (actual type).
    • 比如, o 的实际类型是 GeometricObject, 因为 o 指向使用 new GeometricObject()创建的对象o.

7.2 动态绑定

重点来了: 调用哪个toString() 方法由 o 的实际类型决定. 这称为动态绑定 (dynamic binding).

image_1bflhps75pgv1jfp1uc311r2f619.png-38.4kB

上图描述了动态绑定工作过程: 假设对象 o 是类 C1, C2, ... , Cn的实例, 其中 C1是C2的子类, Cn-1 是 Cn 的子类. Cn是最通用的类(所有类的祖宗), C1 是最特殊的子类. 如果对象o调用一个方法p, 那么JVM会依次在类C1, C2, Cn去查找方法p的实现, 直到找到为止. 一旦找到一个实现就停止查找, 然后调用这个首先找到的实现.

7.3 动态绑定例子

代码清单: DynamicBindingDemo.java

  1. public class DynamicBindingDemo {
  2. public static void main(String[] args) {
  3. m(new GraduateStudent());
  4. m(new Student());
  5. m(new Person());
  6. m(new Object());
  7. }
  8. public static void m(Object x) {
  9. System.out.println(x.toString());
  10. }
  11. }
  12. class GraduateStudent extends Student {
  13. }
  14. class Student extends Person {
  15. public String toString() {
  16. return "Student";
  17. }
  18. }
  19. class Person extends Object {
  20. public String toString() {
  21. return "Person";
  22. }
  23. }

image_1bfljkt27qam1v4q1gp510uihfc13.png-26kB

方法 m() 采用 Object 类型的参数。 可以传递任何对象 (例如:
new GraduateStudent(), new Student(), new Person() 和 new Object() )作为参数来调用 m() 方法。
当执行方法 m(Object x) 时, 调用参数 x 的 toString() 方法。 x 可能会是 GraduateStudent、Student、 Person 或者 Object 的实例.

类 GraduateStudent、 Student、 Person 以及 Object 都重写了 toString() 方法。 使用哪个实现取决于运行时 x 的实际类型. 调用 m( new GraduateStudent() ) 会导致定义在 Student 类中的 toString() 方法被调用.

调 用 m(new Student()) 会调用在 Student 类中定义的 toString()方法. 调用 m(new Person())会调用在 Person 类中定义的 toString()方法. 调用 m(new Object()) 会调用在 Object 类中定义的 toString() 方法.

匹配方法的签名和绑定方法的实现是两个不同的问题.引用变虽的声明类型决定了编译时匹配哪个方法.

在编译时, 编译器会根据参数类型, 参数个数和参数顺序找到匹配的方
法. 一个方法可能在沿着继承链的多个类中实现运行时的动态绑定方法, 这是由变量的实际类型决定的.

思考题

  1. public class Test {
  2. public static void main(String[] args) {
  3. Integer[] list1 = {12, 24, 55, 1};
  4. Double[] list2 = {12.4, 24.0, 55.2, 1.0};
  5. int[] list3 = {1, 2, 3};
  6. printArray(list1);
  7. printArray(list2);
  8. printArray(list3);
  9. }
  10. public static void printArray(Object[] list) {
  11. for (Object o: list)
  12. System.out.print(o + " ");
  13. System.out.println();
  14. }
  15. }
  1. public class Test {
  2. public static void main(String[] args) {
  3. A a = new A(3);
  4. }
  5. }
  6. class A extends B {
  7. public A(int t) {
  8. System.out.println("A's constructor is invoked");
  9. }
  10. }
  11. class B {
  12. public B() {
  13. System.out.println("B's constructor is invoked");
  14. }
  15. }

习题

1. Time时间类设计

设计一个名为Time的类. 这个类包含:

要求:

  1. 画出该类的UML图并实现这个类.
  2. 编写一个测试程序, 创建两个Time对象(使用new Time()和new Time(555550000)), 然后显示它们的小时, 分钟和秒.
  3. 提示:前两个构造方法可以从流逝的时间中提取出小时, 分钟和秒. 对于无参构造方法, 当前时间可以使用System. currentTimeMills() 获取当前时间.

2. Mylnteger整形类设计

设计一个名为Mylnteger的类. 这个类包括:

要求:

  1. 画出该类的 UML 图并实现这个类.
  2. 编写测试程序测试这个类中的所有方法.

3. MyPoint点类设计

设计一个名为MyPoint的类, 表示一个带x坐标和y坐标的点. 该类包括:

要求:

  1. 画出该类的 UML 图并实现这个类.
  2. 编写一个测试程序, 创建两个点(0, 0) 和(10, 30.5), 并显示它们之间的距离.

4. ATM机类设计

使用上次作业创建的 Account 类来模拟一台 ATM 机.

image_1befbqbnokogkctn5u1p0n1auj9.png-93kB

5. Tax财务税款类设计

使用数组编写一个计算税款的程序. 设计一个名为Tax的类, 该类包含下面的实例数据域:

要求:

image_1befca9021k5t1d7p4jbne31pq8m.png-114.2kB

image_1befcc9q81k03vg91lon7nssrk13.png-89kB

6. Circle2D 圆类设计

定义 Circle2D 类, 包括:

image_1befcj3ck1m491tht1vhlgnidfb1g.png-21kB

要求:

  1. 画出该类的 UML 图并实现这个类.
  2. 编写测试程序, 创建一个 Circle2D 对象 c1 (new Circle2D(2, 2, 5.5)), 显示它的面积和周长, 还要显示 c1.contains(3, 3), c1.contains(new Circle2D(4, 5, 10.5)) 和 c1.overlaps(new Circle2D(3, 5, 2.3)).

7. Triangle2D三角形类设计

定义 Triangle2D类, 包含:

image_1befcmbe36rqcom1vo2sqn17sa1t.png-26.5kB

要求:

  1. 画出该类的 UML 图, 并实现这个类.
  2. 编写测试程序, 使用构造方法 new Triangle2D(new MyPoint(2.5, 2), new MyPoint(4.2, 3), new MyPoint(5, 3.5))对象t1, 并显示它的面积和周长
  3. 显示t1.contains(3,3) 和 r1.contains(new Triangle2D(new MyPoint(2.9, 2), new MyPoint(4, 1), MyPoint(1, 3.4)))
  4. 显示t1.overlaps(new Triangle2D(new MyPoint(2, 5.5), new MyPoint(4,-3), MyPoint(2, 6.5)))的结果.

8. MyRectangle2D矩形类设计

定义 MyRectangle2D 类, 包含:

image_1befcpp295971gvac2tqur13el2a.png-17.7kB

要求:

  1. 画出该类的 UML 图, 并实现这个类.
  2. 编写测试程序, 使用构造方法, 创建一个 MyRectangle2D 对象 r1 (new MyRectangle2D(2, 2, 5.5, 4.9) ), 显示它的面积和周长.
  3. 显示r1.contains(3, 3)的结果
  4. 显示r1.contains(new MyRectangle2D(4, 5, 10.5, 3.2))的结果.
  5. 显示r1.overlaps(new MyRectangle2D(3, 5, 2.3, 5.4))的结果.

9. MyDate日期类设计

设计一个名为 MyDate 的类. 该类包含:

要求:

  1. 画出该类的UML图并实现这个类.
  2. 编写测试程序, 创建一个测试程序, 创建两个 Date对象 (使用 new Date() 和 new MyDate(34355555133101L), 然后显示它们的小时, 分钟和秒.
  3. 提示: 前两个构造方法将从逝去的时间中提取出年, 月, 日. 例如: 如果逝去的时间是 561555550000毫秒, 那么 1987年, 9月, 18日.

10. MyString字符串类设计

Java 库中提供了 String 类, 给出你自己对下面方法的实现( 将新类命名为MyString1):

  1. public MyString1(char[] chars);
  2. public char charAt(int index);
  3. public int length();
  4. public MyString1 substring(int begin, int end);
  5. public MyString1 toLowerCase();
  6. public boolean equals(MyString1 s);
  7. public static MyString1 valueOf(int i);

画出该类的 UML 图并实现这个类. 编写测试程序.

在 Java 库中提供了 String 类, 给出你自己对下面方法的实现( 将新类命名为 MyString2 ):

  1. public MyString2(String s);
  2. public int compare(String s);
  3. public MyString2 substring(int begin);
  4. public MyString2 toUpperCase();
  5. public char[] toChars();
  6. public static MyString2 valueOf(boolean b);

画出该类的 UML 图并实现这个类. 编写测试程序.

11. Triangle三角形类设计

设计一个名为Triangle类扩展GeometricObject类. 该类包括:

image_1bf2a3r3fffa1hfmp5r1pptr2p1g.png-30.7kB

12. Person类及子类

设计一个名为 Person 的类和它的两个名为 Student 和 Employee 的子类. Employee 类又有子类: 教员类 Faculty 和职员类 Staff.
每个人都有姓名, 地址, 电话号码和电子邮件地址.

要求:

  1. 画出这些类的 UML 图并实现这些类.
  2. 编写一个测试程序, 创建 Person, Student, Employee, Faculty 和 Staff, 并且调用它们的 toString()方法.

13. Account类的派生类

定义了一个 Account类来建模一个银行账户. 一个账户有账号, 余额, 年利率, 开户日期等属性, 以及存款和取款等方法. 创建两个检测支票
账户 (checking account) 和储蓄账户 (saving account) 的子类。 支票联户有一个透支限定额, 但储蓄账户不能透支.

画出这些类的UML图并实现这些类. 编写一个测试程序, 创建Account,
SavingsAccount 和 CheckingAccount 的对象, 然后调用它们的toString()方法.

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注