Java入门——继承和多态(中)

组合

和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果.

public class Student { 
 ... 
} 
 
public class Teacher { 
 ... 
} 
 
public class School { 
 public Student[] students; 
 public Teacher[] teachers; 
} 

组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段. 这是我们设计类的一种常用方式之一.

  • 组合表示 has - a 语义
  • 在刚才的例子中, 我们可以理解成一个学校中 "包含" 若干学生和教师.
  • 继承表示 is - a 语义
  • 在上面的 "动物和猫" 的例子中, 我们可以理解成一只猫也 "是" 一种动物.

多态

向上转型

在刚才的例子中, 我们写了形如下面的代码

Bird bird = new Bird("圆圆"); 

这个代码也可以写成这个样子

Bird bird = new Bird("圆圆"); 
Animal bird2 = bird; 
 
// 或者写成下面的方式 
Animal bird2 = new Bird("圆圆");

此时 bird2 是一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为 向上转型.

为啥叫 "向上转型"?

在面向对象程序设计中, 针对一些复杂的场景(很多类, 很复杂的继承关系), 我们会画一种 UML 图的方式来表 示类之间的关系. 此时父类通常画在子类的上方. 所以我们就称为 "向上转型" , 表示往父类的方向转.

向上转型发生的时机:

直接赋值 方法传参 方法返回

直接赋值的方式我们已经演示了. 另外两种方式和直接赋值没有本质区别.

方法传参

public class Test { 
 public static void main(String[] args) { 
 Bird bird = new Bird("圆圆"); 
 feed(bird); 
 } 
 
 public static void feed(Animal animal) { 
 animal.eat("谷子"); 
 } 
} 
 
// 执行结果 
圆圆正在吃谷子 

此时形参 animal 的类型是 Animal (基类), 实际上对应到 Bird (父类) 的实例.

方法返回

public class Test { 
 public static void main(String[] args) { 
 Animal animal = findMyAnimal(); 
 } 
 
 public static Animal findMyAnimal() { 
 Bird bird = new Bird("圆圆"); 
 return bird; 
 } 
}

此时方法 findMyAnimal 返回的是一个 Animal 类型的引用, 但是实际上对应到 Bird 的实例.

动态绑定

当子类和父类中出现同名方法的时候, 再去调用会出现什么情况呢?

// Animal.java 
public class Animal { 
 protected String name; 
 
 public Animal(String name) { 
 this.name = name; 
 } 
 
 public void eat(String food) { 
 System.out.println("我是一只小动物"); 
 System.out.println(this.name + "正在吃" + food); 
 } 
}
// Bird.java 
public class Bird extends Animal { 
 public Bird(String name) { 
 super(name); 
 } 
 
 public void eat(String food) { 
 System.out.println("我是一只小鸟"); 
 System.out.println(this.name + "正在吃" + food); 
 } 
} 
 
// Test.java 
public class Test { 
 public static void main(String[] args) { 
 Animal animal1 = new Animal("圆圆"); 
 animal1.eat("谷子"); 
 Animal animal2 = new Bird("扁扁"); 
 animal2.eat("谷子"); 
 } 
} 
 
// 执行结果 
我是一只小动物 
圆圆正在吃谷子 
我是一只小鸟 
扁扁正在吃谷子
  • animal1 和 animal2 虽然都是 Animal 类型的引用, 但是 animal1 指向 Animal 类型的实例, animal2 指向 Bird 类型的实例.
  • 针对 animal1 和 animal2 分别调用 eat 方法, 发现 animal1.eat() 实际调用了父类的方法, 而 animal2.eat() 实际调用了子类的方法.

因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引 用指向的是父类对象还是子类对象.

这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定.

方法重写

针对刚才的 eat 方法来说: 子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖

1. 重写和重载完全不一样. 不要混淆

 2. 普通方法可以重写, static 修饰的静态方法不能重写.

3. 重写中子类的方法的访问权限不能低于父类的方法访问权限.

4. 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外).

如果将子类的 eat 改成 private

// 编译出错 
Error:(8, 10) java: com.csdn.Bird中的eat(java.lang.String)无法覆盖com.bit.Animal中的
eat(java.lang.String) 
 正在尝试分配更低的访问权限; 以前为public

另外, 针对重写的方法, 可以使用 @Override 注解来显式指定.

// Bird.java 
public class Bird extends Animal { 
 @Override 
 private void eat(String food) { 
 ... 
 } 
} 

有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发 现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.

 我们推荐在代码中进行重写方法时显式加上 @Override 注解.

事实上, 方法重写是 Java 语法层次上的规则, 而动态绑定是方法重写这个语法规则的底层实现. 两者本质上描述 的是相同的事情, 只是侧重点不同.

理解多态

有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态的形式来设计程序了. 我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况.

class Shape { 
 public void draw() { 
 // 啥都不用干 
 } 
} 
 
class Cycle extends Shape { 
 @Override 
 public void draw() { 
 System.out.println("○"); 
 } 
} 
 
class Rect extends Shape { 
 @Override 
 public void draw() { 
 System.out.println("□"); 
 } 
} 
 
class Flower extends Shape { 
 @Override 
 public void draw() { 
 System.out.println("♣"); 
 } 
} 
 
/我是分割线// 
 
// Test.java 
public class Test { 
 public static void main(String[] args) { 
 Shape shape1 = new Flower(); 
 Shape shape2 = new Cycle(); 
 Shape shape3 = new Rect(); 
 drawMap(shape1); 
 drawMap(shape2); 
 drawMap(shape3); 
 } 
 // 打印单个图形 
 public static void drawShape(Shape shape) { 
 shape.draw(); 
 } 
} 

在这个代码中, 分割线上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的.

当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当 前的 shape 引用指向的是哪个类型(哪个子类)的实例.

此时 shape 这个引用调用 draw 方法可能会有多种不同的表现 (和 shape 对应的实例相关), 这种行为就称为 多态.

使用多态的好处是什么?

1) 类调用者对类的使用成本进一步降低.

  • 封装是让类的调用者不需要知道类的实现细节.
  • 态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.

2) 能够降低代码的 "圈复杂度", 避免使用大量的 if - else

例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下:

public static void drawShapes() { 
 Rect rect = new Rect(); 
 Cycle cycle = new Cycle(); 
 Flower flower = new Flower(); 
 String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"}; 
 
 for (String shape : shapes) { 
 if (shape.equals("cycle")) { 
 cycle.draw(); 
 } else if (shape.equals("rect")) { 
 rect.draw(); 
 } else if (shape.equals("flower")) { 
 flower.draw(); 
 } 
 } 
} 

如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单.

public static void drawShapes() { 
 // 我们创建了一个 Shape 对象的数组. 
 Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), 
 new Rect(), new Flower()}; 
 for (Shape shape : shapes) { 
 shape.draw(); 
 } 
} 

3) 可扩展能力更强.

如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.

class Triangle extends Shape { 
 @Override 
 public void draw() { 
 System.out.println("△"); 
 } 
} 

对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.

而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高.

向下转型

向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象.

相比于向上转型来说, 向下转型没那么常见, 但是也有一定的用途.

// Animal.java 
public class Animal { 
 protected String name; 
 
 public Animal(String name) { 
 this.name = name; 
 } 
 
 public void eat(String food) { 
 System.out.println("我是一只小动物"); 
 System.out.println(this.name + "正在吃" + food); 
 } 
} 
 
// Bird.java 
public class Bird extends Animal { 
 public Bird(String name) { 
 super(name); 
 } 
 
 public void eat(String food) { 
 System.out.println("我是一只小鸟"); 
 System.out.println(this.name + "正在吃" + food); 
 } 
 
 public void fly() { 
 System.out.println(this.name + "正在飞"); 
 } 
} 
Animal animal = new Bird("圆圆"); 
animal.eat("谷子"); 
 
// 执行结果 
圆圆正在吃谷子 

接下来我们尝试让圆圆飞起来

animal.fly(); 
 
// 编译出错 
找不到 fly 方法 

编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法.

虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的.

对于 Animal animal = new Bird("圆圆") 这样的代码,

编译器检查有哪些方法存在, 看的是 Animal 这个类型

执行时究竟执行父类的方法还是子类的方法, 看的是 Bird 这个类型.

那么想实现刚才的效果, 就需要向下转型.

// (Bird) 表示强制类型转换 
Bird bird = (Bird)animal; 
bird.fly(); 
 
// 执行结果 
圆圆正在飞 

但是这样的向下转型有时是不太可靠的. 例如

Animal animal = new Cat("小猫"); 
Bird bird = (Bird)animal; 
bird.fly(); 
 
// 执行结果, 抛出异常 
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Bird 
 at Test.main(Test.java:35) 

animal 本质上引用的是一个 Cat 对象, 是不能转成 Bird 对象的. 运行时就会抛出异常.

所以, 为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换

Animal animal = new Cat("小猫"); 
if (animal instanceof Bird) { 
 Bird bird = (Bird)animal; 
 bird.fly(); 
} 

instanceof 可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较安全了.

super 关键字

前面的代码中由于使用了重写机制, 调用到的是子类的方法. 如果需要在子类内部调用父类方法怎么办? 可以使用 super 关键字.

super 表示获取到父类实例的引用.

涉及到两种常见用法.

1) 使用了 super 来调用父类的构造器

public Bird(String name) { 
 super(name); 
} 

2) 使用 super 来调用父类的普通方法

public class Bird extends Animal { 
 public Bird(String name) { 
 super(name); 
 } 
 
 @Override 
 public void eat(String food) { 
 // 修改代码, 让子调用父类的接口. 
 super.eat(food); 
 System.out.println("我是一只小鸟"); 
 System.out.println(this.name + "正在吃" + food); 
 } 
} 

在这个代码中, 如果在子类的 eat 方法中直接调用 eat (不加super), 那么此时就认为是调用子类自己的 eat (也就是递 归了). 而加上 super 关键字, 才是调用父类的方法.

总结

多态是面向对象程序设计中比较难理解的部分. 我们会在后面的抽象类和接口中进一步体会多态的使用. 重点是多态带 来的编码上的好处.

另一方面, 如果抛开 Java, 多态其实是一个更广泛的概念, 和 "继承" 这样的语法并没有必然的联系.

  • C++ 中的 "动态多态" 和 Java 的多态类似. 但是 C++ 还有一种 "静态多态"(模板), 就和继承体系没有关系了.
  • Python 中的多态体现的是 "鸭子类型", 也和继承体系没有关系.
  • Go 语言中没有 "继承" 这样的概念, 同样也能表示多态.

无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要方式.

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值