承接上篇 链接: link.
向上转型
通常我们在实例化对象的时候都是通过new关键字来做的,来调用类内的方法。
public class Main {
public static void main(String[] args) {
Animal animal = new Animal("animal");
animal.eat();
Dog dog = new Dog("dog");
dog.eat();
dog.bark();
Cat cat = new Cat("cat");
cat.eat();
Bird bird = new Bird("bird");
bird.eat();
bird.fly();
}
}
//执行结果
animal正在吃
dog正在吃
dog正在汪汪汪
cat正在吃
bird正在吃
bird正在飞 ︿( ̄︶ ̄)︿
那么什么是向上转型呢?
直接赋值法
public class Main {
public static void main(String[] args) {
Animal bird = new Bird("Bird"); //可以new Bird对象用Animal接收
Animal dog = new Dog("Dog");
/* 也可以这样直接赋值
Bird bird = new Bird("Bird");
Animal bird1 = bird;
Dog dog = new Dog("Dog");
Animal dog1 = dog;
/*
此时 bird和 dog 是一个父类 (Animal) 的引用, 指向一个子类 Bird 与 dog的实例. 这种写法称为向上转型
*/
bird.eat();//Bird正在吃
dog.eat();//Dog正在吃
dog.fly(); //报错
bird.bark(); //报错
/*
为啥会报错,因为此时是父类Animal的引用,fly与bark是bird与dog特有的属性方法,通过animal引用是不能够访问的。
*/
}
}
方法传参法
public class Main {
public static void func(Animal animal) {
animal.eat();
}
public static void main(String[] args) {
Dog dog = new Dog("狗狗");
Bird bird = new Bird("鸟");
func(dog);
func(bird);
//执行结果
狗狗正在吃
鸟正在吃
}
}
方法返回
public class Main {
public static Animal func() {
Bird bird = new Bird("鸟");
return bird;
}
public static Animal func1() {
Dog dog = new Dog("狗狗");
return dog;
}
public static void main(String[] args) {
Animal bird = func();
Animal dog = func1();
bird.eat();
dog.eat();
//执行结果
鸟正在吃
狗狗正在吃
}
}
向下转型
向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象. 相比于向上转型来说, 向下转型没那么常见,但是也有一定的用途。
Animal bird = new Bird("Bird"); //可以new Bird对象用Animal接收
Animal dog = new Dog("Dog");
dog.fly(); //报错
bird.bark(); //报错
我们知道此时是调用不到fly与bark方法的。
编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly与bark 方法.
虽然 animal 实际引用的是一个 Bird 对象与Dog方法, 但是编译器是以 animal 的类型来查看有哪些方法的.
对于 Animal bird = new Bird(“鸟”) 这样的代码,
编译器检查有哪些方法存在, 看的是 Animal 这个类型
执行时究竟执行父类的方法还是子类的方法, 看的是 Bird 这个类型.
那么想实现刚才的效果, 就需要向下转型
public static void main(String[] args) {
Animal animal = new Bird("鸟");
Bird bird = (Bird)animal;//强制类型转换
bird.fly();
bird.eat();
Animal animal1 = new Dog("狗狗");
Dog dog = (Dog)animal1;强制类型转换
dog.bark();
dog.eat();
// 执行结果
鸟正在飞 ︿( ̄︶ ̄)︿
鸟正在吃
狗狗正在汪汪汪
狗狗正在吃
//一下示例错误写法
//编译的时候不会报错 但是运行时出错了
Bird bird = (Bird) new Animal("鸟");
bird.eat();
bird.fly();
Dog dog = (Dog) new Animal("狗狗");
dog.eat();
dog.bark();
//Exception in thread "main" java.lang.ClassCastException: animals.Animal cannot be cast to animals.Bird
//at animals.Main.main(Main.java:22)
//还有这样也是错误的
Animal animal = new Dog("狗狗");
Bird bird = (Bird)animal;
bird.fly();
// 执行结果, 抛出异常
Exception in thread "main" java.lang.ClassCastException: Dog cannot be cast to Bird
at Test.main(Test.java:35)
方法重写
针对刚才的 eat 方法来说:
子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override)。
- 重写和重载完全不一样. 不要混淆
- 普通方法可以重写, static 修饰的静态方法不能重写.
- 重写中子类的方法的访问权限不能低于父类的方法访问权限.
- 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外).
public class Animal {
public void eat() {
...
}
}
public class Bird extends Animal {
public void eat() {
...
}
}
public class Dog extends Animal {
public void eat() {
...
}
}
//方法权限示例: 将子类的 eat 改成 private
public class Bird extends Animal {
// 将子类的 eat 改成 private
private void eat() {
...
}
}
// 编译出错
Error:(8, 10) java: com.bit.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 注解.
*/
super 关键字
前面的代码中由于使用了重写机制, 调用到的是子类的方法. 如果需要在子类内部调用父类方法怎么办? 可以使用super 关键字
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println( "animal 正在吃");
}
}
public class Bird extends Animal{
//使用了 super 来调用父类的构造器
public Bird(String name) {
super(name);
}
@Override
public void eat() {
// 修改代码, 让子调用父类的接口.
super.eat(food);
System.out.println("我是一只小鸟");
System.out.println(this.name + "正在吃" + food);
/*
在这个代码中, 如果在子类的 eat 方法中直接调用 eat (不加super), 那么此时就认为是调用子类自己的 eat (也就是递
归了). 而加上 super 关键字, 才是调用父类的方法
*/
}
}
public class Main {
public static void main(String[] args) {
Bird bird = new Bird("鸟");
bird.eat();
}
}
//执行结果
animal 正在吃 //调用父类的方法
我是一只小鸟
鸟正在吃
理解多态
例:打印多种形状
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("♣");
}
}
public class Test {
// 打印单个图形
public static void drawMap(Shape shape) {
shape.draw();
}
public static void main(String[] args) {
Shape shape1 = new Flower();
Shape shape2 = new Cycle();
Shape shape3 = new Rect();
drawMap(shape1);
drawMap(shape2);
drawMap(shape3);
}
}
//执行结果
♣
○
□
当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为 多态。
使用多态的好处是什么?
- 类调用者对类的使用成本进一步降低.
封装是让类的调用者不需要知道类的实现细节.
多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.
因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低.
这也贴合了 <<代码大全>> 中关于 “管理代码复杂程度” 的初衷. - 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下:
public class Test {
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();
}
}
}
public static void main(String[] args) {
drawShapes();
}
}
//执行结果
○
□
○
□
♣
如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单
public class Test {
public static void drawShapes() {
// 我们创建了一个 Shape 对象的数组.
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
new Rect(), new Flower()};
for (Shape shape : shapes) {
shape.draw();
}
}
public static void main(String[] args) {
drawShapes();
}
}
//执行结果
○
□
○
□
♣