【java】多态、抽象类、接口

多态概述

一种思想方法,多态落实到代码上具体的体现形式,就是通过以下三点来体现的:

  • 1.向上转型
  • 2.动态绑定
  • 3.方法重写

接下来都以下代码举例:

//Animal.java
public class Animal {
    protected String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat(String food) {
        System.out.println(name + "正在吃"+food);
    }
}
//Bird.java
public class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }
    public void fly() {
        System.out.println(name + "正在飞");
    }
    @Override
    public void eat(String food) {
        System.out.println("我是一只小鸟");
        System.out.println(name + "正在吃" + food);
    }
}
//Cat.java
public class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }
}
向上转型
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("谷子");
	}
}
// 执行结果
鸟鸟正在吃谷子
方法返回向上转型
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 的实例。

动态绑定
// 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 ()方法来说:
子类实现父类的同名方法,并且 参数的类型和个数完全相同 ,这种情况称为 覆写/重写/覆盖(Override)

关于重写的注意事项

  • 重写和重载完全不一样. 不要混淆
  • 普通方法可以重写, static 修饰的静态方法不能重写.
  • 重写中子类的方法的访问权限不能低于父类的方法访问权限
  • 针对重写的方法,可以使用 @Override 注解来显式指定。(有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet),那么此时编译器就会发现父类中没有 aet 方法,就会编译报错,提示无法构成重写)

示例:将子类的eat方法改为private:(编译出错)

// Animal.java
public class Animal {
	public void eat(String food) {
		...
	}
}
// Bird.java
public class Bird extends Animal {
	// 将子类的 eat 改成 private
	private void eat(String food) {
		...
	}
}
// 编译出错
Error:(8, 10) java: com.bit.Bird中的eat(java.lang.String)无法覆盖com.bit.Animal中的
eat(java.lang.String)

理解多态

多态可概括为, 就是 “一个引用,能表现出多种不同形态”。
使用多态的好处是什么?
1)类调用者对类的使用成本进一步降低

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

因此,多态可以理解成是封装的更进一步,让类调用者对类的使用成本进一步降低
2)能够降低代码的 “圈复杂度”,避免使用大量的 if - else
3)可扩展能力更强。

向下转型(instanceof)

向上转型是子类对象转成父类对象,向下转型就是父类对象转成子类对象. 相比于向上转型来说,向下转型没那么常见。
示例如下:

Animal animal = new Bird("鸟鸟");
animal.eat("谷子");

// (Bird) 表示强制类型转换
Bird bird = (Bird)animal;
bird.fly();

但是这样转不一定可靠,如下代码:

Animal animal = new Cat("小猫");
Bird bird = (Bird)animal;
bird.fly();

animal 本质上引用的是一个 Cat 对象,是不能转成 Bird 对象的. 运行时就会抛出异常。为此,我们用instanceof关键字判定一个引用是否是某个类的实例。如果是,则返回 true,这时再进行向下转型就比较安全了。

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

抽象类

如果父类没有什么实际工作,我们可以把它设计成一个 抽象方法(abstract method),包含抽象方法的类我们称为 抽象类(abstract class)

abstract class Shape {
	abstract public void draw();
}
  • 在 draw 方法前加上 abstract 关键字,表示这是一个抽象方法. 同时抽象方法没有方法体(没有 { },不能执行具体代码)。
  • 对于包含抽象方法的类,必须加上 abstract 关键字表示这是一个抽象类。

注意事项:

  • 1.抽象类不能直接实例化
  • 2.抽象方法不能是 private 的
  • 3.抽象类中 可以包含其他的非抽象方法,也可以包含字段。这个非抽象方法和普通方法的规则都是一样的,可以被重写,也可以被子类直接调用。

抽象类示例:

abstract class Shape {
    abstract public void draw();
    void func() {
        System.out.println("func");
    }
}
class Rect extends Shape {
    @Override
    public void draw() {
        System.out.println("□");
    }
}
class Cycle extends Shape {
    @Override
    public void draw() {
        System.out.println("○");
    }
}
public class Test {
    public static void main(String[] args) {
        Shape shape = new Rect();
        shape.draw();
        shape.func();
    }
}

抽象类的作用
  抽象类存在的最大意义就是为了被继承。抽象类本身不能被实例化,要想使用,只能创建该抽象类的子类,然后让子类重写抽象类中的抽象方法。
  普通的类也可以被继承呀,普通的方法也可以被重写呀,为什么还要有抽象类和抽象方法呢?
  回答:使用抽象类相当于多了一重编译器的校验,实际工作不应该由父类完成,而应由子类完成。那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的。但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题。很多语法存在的意义都是为了 “预防出错”。

接口

接口是抽象类的更进一步。抽象类中还可以包含非抽象方法和字段,而接口中包含的方法都是抽象方法,,字段只能包含静态常量

上面打印图形的代码设计成一个接口:

interface IShape {
	void draw();
	public static final int num = 10;
}
class Cycle implements IShape {
	@Override
	public void draw() {
		System.out.println("○");
	}
}
public class Test {
	public static void main(String[] args) {
		IShape shape = new Rect();
		shape.draw();
	}
}
  • 使用 interface 定义一个接口
  • 接口中的方法一定是抽象方法, 因此可以省略 abstract
  • 接口中的方法一定是 public, 因此可以省略 public
  • Cycle 使用 implements 继承接口. 此时表达的含义不再是 “扩展”, 而是 “实现”
  • 调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例.
  • 接口不能单独被实例化.
  • IShape中字段的 public, static, final 的关键字都可以省略. 省略后的 num 仍然表示 public 的静态常量

实现多个接口

有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的。然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果
下列代码,鸭子水陆空三栖:

class Animal {
	protected String name;
	public Animal(String name) {
		this.name = name;
	}
}
interface IFlying {
	void fly();
}
interface IRunning {
	void run();
}
interface ISwimming {
	void swim();
}
class Duck extends Animal implements IRunning, ISwimming, IFlying {
	public Duck(String name) {
		super(name);
	}
	@Override
	public void fly() {
		System.out.println(this.name + "正在用翅膀飞");
	}
	@Override
	public void run() {
		System.out.println(this.name + "正在用两条腿跑");
	}
	@Override
	public void swim() {
		System.out.println(this.name + "正在漂在水上");
	}
}

上面的代码展示了 Java 面向对象编程中最常见的用法:一个类继承一个父类,同时实现多种接口。
继承表达的含义是 is - a 语义,而接口表达的含义是 具有 xxx 特性

接口间的继承

接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字

interface IRunning {
	void run();
}
interface ISwimming {
	void swim();
}
//两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming {

}
class Frog implements IAmphibious {
	...
}

应用接口的实例

class Student implements Comparable {
    private String name;
    private int score;
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
    @Override
    public String toString() {
        return "[" + this.name + ":" + this.score + "]";
    }
    @Override
    public int compareTo(Object o) {
        Student s = (Student)o;
        if (this.score > s.score) {
            return -1;
        } else if (this.score < s.score) {
            return 1;
        } else {
            return 0;
        }
    }
}
public class Test {
    public static void main(String[] args) {
        Student[] students = new Student[] {
                new Student("张三", 95),
                new Student("李四", 96),
                new Student("王五", 97),
                new Student("赵六", 92),
        };

        Arrays.sort(students);
        System.out.println(Arrays.toString(students));
    }
}

在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象。
然后比较当前对象和参数对象的大小关系(按分数来算)。

  • 如果当前对象应排在参数对象之前, 返回小于 0 的数字;
  • 如果当前对象应排在参数对象之后, 返回大于 0 的数字;
  • 如果当前对象和参数对象不分先后, 返回 0;

注意事项: 对于 sort 方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备 compareTo 这样的能力. 通过重写 compareTo 方法的方式, 就可以定义比较规则。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值