java面向对象编程_多态_抽象类_接口
多态
在编程语言和类型论中,多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口。 多态类型(英语:polymorphic type)可以将自身所支持的操作套用到其它类型的值上。多态(百度词条)
向上转型
子类对象赋值给了父类引用
该对象只能访问父类的字段和方法!
直接赋值
子类对象赋值给了父类引用
class Animal{
protected String name;
protected int age;
public void eat(){
System.out.println("animal eat()!");
}
}
class Dog extends Animal{
protected int height;
public void running(){
System.out.println("dog running()!");
}
}
public class Test_1 {
public static void main(String[] args) {
Animal animal = new Animal();
animal = new Dog(); //1.子类对象赋值给了父类引用
Animal animal1 = new Dog(); //2.和 1 等价
animal.eat(); //调用父类中的方法
//error animal只能访问父类中的字段和方法!
animal.height=1;
animal.running();
}
}
方法传参
//方法传参
class Animal{
protected String name;
protected int age;
public void eat(){
System.out.println("animal eat()!");
}
}
class Dog extends Animal{
protected int height;
public void running(){
System.out.println("dog running()!");
}
}
public class Test_2 {
public static void func(Animal animal){
animal.eat();
}
public static void main(String[] args) {
Dog dog = new Dog();
func(dog);
func(new Dog()); //子类对象赋值给了父类引用!
}
}
方法返回
//方法返回
class Animal{
protected String name;
protected int age;
public void eat(){
System.out.println("animal eat()!");
}
}
class Dog extends Animal{
protected int height;
public void running(){
System.out.println("dog running()!");
}
}
public class Test_2 {
public static Animal func(){
return new Dog(); //子类对对象返回给了父类引用!
}
public static void main(String[] args) {
Animal animal = func();
animal.eat();
}
}
动态绑定
动态绑定就是,当子类重写了父类的方法时,向上转型后,对象调用与重写的方法,访问的是子类对象中的重写方法!
//运行时绑定
class Animal{
protected String name;
protected int age;
public void eat(){
System.out.println("animal eat()!");
}
}
class Dog extends Animal{
protected int height;
@Override
public void eat() {
System.out.println("dog eat()!");
}
public void running(){
System.out.println("dog running()!");
}
}
public class Test_2 {
public static void main(String[] args) {
Animal animal = new Dog();
animal.eat(); //运行时绑定!
}
}
为啥要叫运行时绑定呢?
难道说编译的时候没有绑定?
确实如此,当我们查看java
的反汇编代码时就会发现,编译期间anmial
调用的是自己的eat
方法,但是运行时却绑定了子类的eat()
!
java
反汇编代码步骤:
- 找到,我们需要反汇编代码类的字节码文件
- 在命令符的窗口下,输入
javap -c 类名
代码 回车即可!
理解多态
我们想一想多态可以帮助我们做些什么!
bug郭的理解
- 运行时绑定使用场景
我们可以重写父类的方法,实现多态。调用重写的方法,一个父类可以有多个子类,不同的子类重写了不同的方法,实现了真正意义上的多态!
eg:打印图形
//类的实现者
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_1{
public static void main(String[] args) {
Shape shape1 = new Flower();
Shape shape2 = new Cycle();
Shape shape3 = new Rect();
drawShape(shape1);
drawShape(shape2);
drawShape(shape3);
}
// 打印单个图形
public static void drawShape(Shape shape) {
shape.draw();
}
}
使用多态的好处是什么?
- 类调用者对类的使用成本进一步降低。
封装是让类的调用者不需要知道类的实现细节。
多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可。
因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低。
- 能够降低代码的 “圈复杂度”, 避免使用大量的
if - else
。
“圈复杂度” :就是代码中的分支和循环; - 可扩展能力更强。
如果要新增一种新的形状,使用多态的方式代码改动成本也比较低。
向下转型
我们知道向上转型是子类对象赋值给了父类引用!
那向下转型莫不就是:父类对象赋值给了子类引用~
并不常见~了解一下即可!
//向下转型
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);
}
}
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 + "正在飞");
}
}
public class Test_2 {
public static void main(String[] args) {
Animal animal = new Bird("鸽鸽"); //先借助向上转型
animal.eat("脐橙");
//animal.fly; //error
Bird bird;
bird = (Bird)animal; //向下转型需要强转
bird.fly();
}
}
可以看到向下转型步骤比较繁琐,通常要借助向上转型!
而且我们需要确定是否为父子类关系!避免异常!
利用
instanceof
关键字可以判定一个引用是否是某个类的实例。
若真返回true
,若假返回false
!
构造方法中调用一个重写的方法(一个坑!)
//坑
class B {
public B() {
// do nothing
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test_3{
public static void main(String[] args) {
D d = new D();
}
}
bug郭
看了半天愣是没整明白为啥这个代码运行结果是这样!!!
我的理解:创建子类对象d会调用自己的构造方法,子类要先帮助父类构造,而父类中调用了子类重写的方法,动态绑定了;
我们并有执行子类中的 private int num = 1;
语句!所以num
此时并没有赋值!所以为0
!
正解
- 构造
D
对象的同时, 会调用B
的构造方法. B
的构造方法中调用了func
方法, 此时会触发动态绑定, 会调用到D
中的func
- 此时
D
对象自身还没有构造, 此时num
处在未初始化的状态, 值为0
.
结论: “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏但又及难发现的问题!
抽象类
基本语法
在刚才的打印图形例子中, 我们发现, 父类Shape
中的 draw
方法好像并没有什么实际工作, 主要的绘制图形都是由Shape
的各种子类的 draw
方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstractmethod)
, 包含抽象方法的类我们称为 抽象类(abstract class)
。
abstract class Shape {
abstract public void draw();
}
在 draw
方法前加上abstract
关键字, 表示这是一个抽象方法。同时抽象方法没有方法体(没有{ }
, 不能执行具体代码)。
对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类。
注意事项
- 抽象类不能直接实例化。
- 抽象类可以有一般类一样的字段和方法,语法相同。
abstract class Shape {
protected int longth;
protected int wide;
public int area(){
return longth*wide;
}
abstract public void draw();
}
-
子类继承父类抽象类,必须重写父类中的抽象方法!
-
抽象方法不能是
private
的
我们知道private
修饰只能在当前类中访问,而抽象方法需要子类实现!
抽象类的作用
抽象类存在的最大意义就是为了被继承。
抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类。然后让子类重写抽象类中的抽象方法。
有些同学可能会说了, 普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢?
确实如此. 但是使用抽象类相当于多了一重编译器的校验。
使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类了,
使用普通类编译器是不会报错的。但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题!
接口
基本语法
//定义接口类型
interface IAnimal{
//抽象方法
// public static final 字段
}
-
使用
interface
定义一个接口
-
接口中的方法一定是抽象方法, 因此可以省略
abstract
接口中的方法一定是public
, 因此可以省略public
-
接口中只能包含抽象方法,对于字段来说, 接口中只能包含静态常量
(final static)
interface IAnimal{
// public static final 字段 可以省略public static final
String name = "animal"; //因为是final 修饰,需要赋初值!
int age = 18;
//抽象方法
public abstract void speak();
void eat(); //省略public abstract
}
-
子类 使用
implements
继承接口。 此时表达的含义不再是 “扩展”, 而是 “实现” -
在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例。
-
接口不能单独被实例化。
-
扩展
(extends)
vs 实现(implements)
扩展指的是当前已经有一定的功能了, 进一步扩充功能。
实现指的是当前啥都没有, 需要从头构造出来。
- 接口不能用限定符修饰
实现多个接口
接口弥补了java
无法多继承的缺陷!
我们可以实现多个接口
基本语法
有时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的。然而 Java 中只支持单继承, 一个类只
extends
一个父类。 但是可以同时实现多个接口, 也能达到多继承类似的效果。
class 类名 implement interfa1,interface2,....interfaceN{
//实现所有接口中的方法!
}
实例一
下面我们通过类来表示张三
//实现多个接口
interface IAnimal{
String name = "animal";
int age = 18;
void speak();
void eat();
}
interface IPeople{
String iQ = "140";
void study();
}
class Zhansan implements IAnimal,IPeople{
@Override
public void speak() {
System.out.println("Speak Chinese!");
}
@Override
public void eat() {
System.out.println("Eat food!");
}
@Override
public void study() {
System.out.println("Study java!");
}
}
public class Test_6 {
public static void main(String[] args) {
Zhansan zhansan = new Zhansan();
zhansan.eat();
zhansan.speak();
zhansan.study();
}
}
实例二
现在我们通过一个类来表示一组动物。
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 Cat extends Animal implements IRunning {
public Cat(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在用四条腿跑");
}
}
鱼, 是会游的。
class Fish extends Animal implements ISwimming {
public Fish(String name) {
super(name);
}
@Override
public void swim() {
System.out.println(this.name + "正在用尾巴游泳");
}
}
青蛙, 既能跑, 又能游(两栖动物)
class Frog extends Animal implements IRunning, ISwimming {
public Frog(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在往前跳");
}
@Override
public void swim() {
System.out.println(this.name + "正在蹬腿游泳");
}
}
提示,
IDEA
中使用ctrl + i
快速实现接口
还有一种神奇的动物, 水陆空三栖, 叫做 “鸭子”
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
特性 。
猫是一种动物, 具有会跑的特性。
青蛙也是一种动物, 既能跑, 也能游泳
鸭子也是一种动物, 既能跑, 也能游, 还能飞
这样设计有什么好处呢?
时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力!
例如, 现在实现一个方法, 叫 “散步”
public static void walk(IRunning running) {
System.out.println("我带着伙伴去散步");
running.run();
}
在这个walk
方法内部, 我们并不关注到底是哪种动物, 只要参数是会跑的, 就行
Cat cat = new Cat("小猫");
walk(cat);
Frog frog = new Frog("小青蛙");
walk(frog);
甚至参数可以不是 “动物”, 只要会跑
class Robot implements IRunning {
private String name;
public Robot(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(this.name + "正在用轮子跑");
}
}
Robot robot = new Robot("机器人");
walk(robot);
接口的使用实例
我们java
系统包中的很多类都实现了很多接口,使得该类具有某种属性
给对象排序!
//创建student类
class Student{
private String name;
private double score;
public Student(String name,double score){
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", score=" + score +
'}';
}
}
public static void main(String[] args) {
//学生对象数组
Student[] students = new Student[]{
new Student("张三",88.7),
new Student("李四",67),
new Student("王五",98),
};
Arrays.sort(students); //排序
System.out.println(students);
}
显然,我们无法排序对象类型的数据,
报异常,说我们Student
类没有实现Comparable
接口!
Comparable
接口中的抽象方法 compareTo
方法详细信息
int compareTo(T o)
将此对象与指定的对象进行比较以进行排序。 返回一个负整数,零或正整数,因为该对象小于,等于或大于指定对象。
实现程序必须确保sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
所有x
和y
。 (这意味着x.compareTo(y)
必须抛出异常if y.compareTo(x)
引发异常。)
实施者还必须确保关系是可传递的: (x.compareTo(y)>0 && y.compareTo(z)>0)表示x.compareTo(z)>0
。
最后,实施者必须确保x.compareTo(y)==0意味着sgn(x.compareTo(z)) == sgn(y.compareTo(z))
,对于所有z
。
强烈建议,但不要严格要求(x.compareTo(y)==0) == (x.equals(y))
。 一般来说,任何实现Comparable
接口并违反这种情况的类应清楚地表明这一点。 推荐的语言是“注意:此类具有与equals
不一致的自然排序”。
在前面的描述中,符号sgn( )
表达式表示数学符号函数,其定义根据表达式的值是否为负,零或正返回的-1
一个,0
,或1
。
参数
o -
要比较的对象。
结果
负整数,零或正整数,因为该对象小于,等于或大于指定对象。
异常
NullPointerException
- 如果指定的对象为空
ClassCastException
- 如果指定的对象的类型阻止它与该对象进行比较。
看到这么多文字,是不是头都大了!
没有关系,bug郭
也头大,不过我知道咋用
//创建student类并且实现Comparable接口
class Student implements Comparable{
private String name;
private double score;
public Student(String name,double score){
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", score=" + score +
'}';
}
@Override //实现compareTo方法
public int compareTo(Object o) {
Student s = (Student)o; //对o强转, o指的是待比较对象
if (this.score > s.score) { //当前对象的score值大
return -1;
} else if (this.score< s.score) { //对象o的score值大
return 1;
} else {
return 0;
}
}
}
我们可以看到,这时排的升序,如果要降序就只需将大于号变成小于号即可!
在 sort
方法中会自动调用compareTo
方法. compareTo
的参数是 Object
, 其实传入的就是 Student
类型的对象.
然后比较当前对象和参数对象的大小关系(按分数来算).
如果当前对象应排在参数对象之前, 返回小于0 的数字;
如果当前对象应排在参数对象之后, 返回大于 0 的数字;
如果当前对象和参数对象不分先后, 返回0;
注意事项: 对于 sort
方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备compareTo
这样的能力. 通过重写compareTo
方法的方式, 就可以定义比较规则。
为了进一步加深对接口的理解, 我们可以尝试自己实现一个sort
方法来完成刚才的排序过程(使用冒泡排序)!
public static void sort(Comparable[] array) {
for (int bound = 0; bound < array.length; bound++) {
for (int cur = array.length - 1; cur > bound; cur--) {
if (array[cur - 1].compareTo(array[cur]) > 0) {
// 说明顺序不符合要求, 交换两个变量的位置
Comparable tmp = array[cur - 1];
array[cur - 1] = array[cur];
array[cur] = tmp;
}
}
}
}
接口间的继承
接口可以继承一个接口, 达到复用的效果. 使用
extends
关键字!
interface IAmphibious extends IRunning, ISwimming {
}
class Frog implements IAmphibious {
@Override
public void run() {
System.out.println("Frog run!");
}
@Override
public void swim() {
System.out.println("Frog swim!");
}
}
public class Test_1 {
public static void main(String[] args) {
Frog frog = new Frog();
frog.run();
frog.swim();
}
}
通过接口继承创建一个新的接口
IAmphibious
表示 “两栖的”. 此时实现接口创建的Frog
类, 就继续要实现run
方法,也需要实现swim
方法!
接口间的继承相当于将多个接口合并在一起!
- 点赞
- 收藏
- 关注作者
评论(0)