Java | abstract关键字面向对象的第三大特征——多态

Posted 烽起黎明

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java | abstract关键字面向对象的第三大特征——多态相关的知识,希望对你有一定的参考价值。

Hello 大家好,讲完了类与对象的两大基本特征,本文就让我们一起进入面向对象的第三大特征——多态,感受多态所带来的魅力🌹

重要又灵活的多态

一、上转型对象的引入

1、什么是向上转型

所谓向上,用一句话来讲就是【父类引用指向子类对象】,对于上转型对象,它可以说是多态的雏形,要学好多态必须先了解上转型对象。好,废话不所说,直接上代码

public class Animal 
    String name;

    public Animal(String name) 
        this.name = name;
    
    void eat()
        System.out.println(this.name + "正在吃东西");
    

public class Cat extends Animal
    public Cat(String name) 
        super(name);
    

    @Override
    void eat() 
        System.out.println(this.name + "正在吃鱼");
    

public class test 
    public static void main(String[] args) 
        Animal animal = new Cat("咪咪");
        animal.eat();
    

  • 从上述代码和运行示例可以看出,对于上转型对象就是父类引用变量调用的方法是子类覆盖或继承父类的方法,而不是父类的方法。但是调用的属性还是父类的属性

2、发生向上转型的三种时机

接下去我们来了解一下产生上转型对象的三种方式,可以可以帮我们在实例开发中快速的判断自己是否在使用上转型对象

  • 第一种,直接赋值,将子类对象给到父类引用

这种的话就是我上面作为引入的案例

Animal animal = new Cat("咪咪");
animal.eat();
  • 第二种,使用方法传参的形式

从下面的代码可以看出,是将一个本类方法的形参设置为父类的引用,然后将子类的对象作为实参传入,这也可以形成上转型对象

public static void func(Animal animal)
    animal.eat();

public static void main(String[] args) 
    Cat cat = new Cat("咪咪");
    func(cat);

  • 第三种,使用返回值形成上转型对象

这种方式比较抽象而且难理解,因为是采用父类这个类型作为返回值,因为直接new一个子类的对象去返回,就自然地形成了上转型对象,这个确实不太好理解,大家要自己观察一下,就不给代码,给的指示图片

3、使用上转型对象的注意要点

说完了三种上转型对象的方式,接下去我们说说使用在这个上转型对象时我们需要注意哪些

  • 上转型对象不可以访问子类新增的成员变量和子类自己新增的方法,因为这是子类独有的,而父类中没有
  • 上转型对象可以访问子类继承的方法或者是子类重写的方法,这个时候当上转型对象去调用这个方法时,一定是调用了子类重写后的方法,这就是我们前面在讲继承的时候所提到的方法重写
  • 不可以将父类创建的对象的引用赋值给子类声明的对象,也就是下面的这两句代码,这很明显和我们的上转型对象相反的,我们是将子类对象给到父类的引用,但这是将父类的引用给到子类的对象,完全就是颠倒黑白【就和猫是动物,动物却不是猫🐱一个道理】
Animal animal;
Cat c = animal;

4、向上转型与接口回调的区别

好,最后我们再来说一下上转型对象和接口回调的区别,接口的话会在下一篇文章讲【implements关键字】的时候讲到,这里先提一嘴

  • 对于向上转型的话,它是牵涉到多态和运行期绑定的范畴,这里的运行绑定我没有讲,因为太偏了,不符合本文的主旨,感兴趣的小伙伴可以去了解一下Java中的动态绑定
  • 而对于这个接口回调的话,它和向上转型非常得类似,是将一个实现某一接口的类创建的对象赋值给该接口声明的接口变量,使得该接口变量可以去调用被类实现的接口方法接口提供的Default方法
  • 好,就不多说,请看我的下一篇文章😀

二、继承与多态的联系

1、多态的基本概念及引入

讲到了多态,也不能忘了继承,接下去就让我们来讲讲继承和多态之间的联系,以及它们都有哪些优点

  • 首先来讲一讲多态的概念,对于多态,就是不同对象对同一物体或事件发出不同的反应或响应,比如student是一个父类,那么在操场上上体育课的学生和在教室里面的学生就是它的子类。这时上课铃声响了,【上体育课的学生】去操场,【在教室里面上课的学生】则是回教室,不同的学生有着不同的反应,这就是多态

然后来看一个小案例

class Animal
	void cry()
	

class Dog extens Animal
	void cry()
		System.out.println("汪汪~~");
	

class Cat extens Animal
	void cry()
		System.out.println("喵喵~~");
	

class test
    public static void main(String[] args) 
		Animal animal;
		animal = new Dog();
		animal.cry();

		animal = new Cat();
		animal.cry();
    

  • 从上述的小案例可以看出,对于继承,它可以实现代码的复用性,将父类中的方法给到继承的子类继续去使用,不需要再重写定义;而对于多态,它能够让类的调用这连这个类的类型都可以不必知道是什么,只需要这个对象具有哪个具体的方法即可,因为只需要去调用具体的方法,就可以通过父类的引用去调用子类对象然后访问子类重写的这个方法

2、要产生多态的条件

了解了多态的基本概念之后,我们要接下来说说要如何去实现多态

  • 要发生向上转型【父类引用 引用子类对象】
  • 子类和父类都要有同名的覆盖方法
  • 通过父类引用调用重写方法时形成运行时绑定

3、多态的好处

清楚了如何产生多态的条件,那我们就要去想,这个多态究竟有什么好处呢,为什么她是面向对象中最重要的一趴,接下来我们来说说这个

  • ①类调用者对类的使用成本进一步降低
  • ②能够降低代码的【圈复杂度】,避免使用大量的if-else
  • 由于这一点好处可能不太好理解,我们讲个具体的案例分析一下
class Geometry
    void draw()
        System.out.println("画一个几何图形");
    

class Circle extends Geometry
    @Override
    void draw() 
        System.out.println("画一个⚪");
    

class Slice extends Geometry
    @Override
    void draw() 
        System.out.println("画一个♦");
    

class Triangle extends Geometry
    @Override
    void draw() 
        System.out.println("画一个▲");
    

public class test2 
    public static void main(String[] args) 
        Circle circle = new Circle();
        Slice slice = new Slice();
        Triangle triangle = new Triangle();
        String[] shapes  = "cycle","triangle","slice","cycle";
        for(String shape : shapes)
        
            if(shape.equals("cycle"))
                circle.draw();
            else if(shape.equals("triangle"))
                triangle.draw();
            else if(shape.equals("slice"))
                slice.draw();
            
        
    

  • 从上述案例可以看出,我们是创建了一个几何图形类,然后用三个不同的几何图形类去继承这个父类,然后重写父类中的draw()方法,在主方法接口中我们可以看到,是通过一个遍历和判断的形式去访问这个Shapes对象数组中的值,通过equals()这个API去判断字符串是否符合,符合的话再用不同的对象去调用各自的方法
  • 从中我们可以看到,这个写法是比较冗杂又常规的,那有没有更好的方法呢?
  • 这个时候就可以使用我们多态的思想了,因为父类和子类中有同样且重名的方法,也进行了重写,我们可以利用将子类对象给到父类引用这个方法,去形成一个上转型对象,通过多态的形式来简化代码

OK,是时候展现真正的技术了😎

Geometry[] shapes = new Circle(),new Triangle(),new Slice(),new Circle();
for(Geometry shape : shapes)
    shape.draw();

  • 对,就是这么简便,我们可以直接在这个对象数组中放new出来的子类对象,然后在遍历这个数组的时候便发生了上转型对象,利用父类的引用shape去调用每一次传入进来的子类对象,就可以去独立地方法那个子类所重写的方法,这就很好地体现了多态的思想

③对于最后这一点,还是比较好理解的,就是可扩展能力强【无限地增加继承的子类】

  • 比如说你要加一个正方形类,只需要把这个类实现一下然后去继承一下Geometry父类即可,完全不需要改动任何的代码,这就是多态的优势之处

看完了上面这些,那您就算初步地了解了多态这个概念,但是并没有形成那个思维,只是一个引入,接下来我们便通过abstract这个关键字真正地进入多态的编程模式,感受面向抽象的编程思维🚶

三、abstract关键字【抽象类与抽象方法】

1、保姆级细致引入!!!

  • 首先我们来分析一下上面Geometry父类,大家有没有发现父类中的这个draw()方法写了和没写一样,因为完全就没有产生这么一个调用
class Geometry
    void draw()
        System.out.println("画一个几何图形");
    

  • 为此我们就可以将此方法设置为抽象方法,在返回值类型前加上一个abstract关键字即可,然后抹去这个方法的方法体,因为抽象方法是不可以有方法体的
abstract void draw();
  • 但是这样写的话编译器马上就给我们报错了,说是一定要将这个类也抽象,这就形成了抽象类的概念,因为如果一个类中有抽象方法,那么这个类就必须是抽象类

2、注意事项

那这时候就有同学说,哇,这个关键字很厉害、很高级的感觉。是的,不然我不会前面铺垫那么多,才讲到这个关键字,厉害归厉害,但是在使用这个关键字的时候要注意的地方还是挺多的,让我们一起来看一下

1. 抽象类不能被实例化

  • 为什么不能被实例化呢?都说了它是抽象的嘛,怎么能会有一个具体的东西呈现给你呢,是吧。所以下面这步操作你不可以做👇
Geometry shape = new Geometry();

2. 类内的数据成员,和普通类没有区别

  • 此话怎讲呢?也就是这个抽象类,它除了不能对抽象方法做实现外,其他其实也和普通的类没什么区别,普通的成员变量和成员方法都是可以定义的
abstract class Geometry
    private int num;
    public void func()
        System.out.println("这是抽象类的一个实例方法");
    
    abstract void draw();

3. 抽象类主要就是用来被继承的,所以不可以被final关键字修饰,抽象方法也是一样,需要被重写

  • 这个的话就要理解,因为抽象类的话已经不可以被实例化对象了,那你再不能继承它然后做一些操作的话,那这个类不就没用了吗,是吧😀,然后这个抽象方法的话,你在抽象类中没有重写它,在继承子类中也没有对其进行一个重写,也是很荒谬的一件事
  • 上一篇文章中我们有详细讲过final关键字,说到了final这个关键字如果去修饰方法的时候,那么这个方法就不可以被重写,如果去修饰类的话,那么这个类就不可以被继承,所以大家一定不要把abstract和final关键字写在一起,这是矛盾的

4. 如果一个类继承了这个抽象类,那么这个类必须重写抽象类中的抽象方法

  • 这个的话在上面也讲到过了,如果一个类去继承了一个抽象类,那么你就必须重写其中的抽象方法,否则的话要么你自己也要定成一个抽象类,然后继承你的子类就必须要重写你父类的方法,就如下面的代码一样
abstract class ff extends Geometry
    


class gg extends ff
    @Override
    void draw() 
        
    

5. 不可以用staicprivate关键字修饰abstract方法

  • 因为static关键字指的是静态方法,是可以被所有同包下的类所调用的,然后其他类就可以重写这个方法自己用,这就违背了继承的特性,只有子类才可以重写父类的方法
  • 然后的话priavate关键字因为是私有的,那子类就是无法访问的,私有的成员变量还可以通过get()方法来访问一下,如果父类的方法都私有化了,那么子类是无法去访问的

6. 抽象类中也可以由构造方法,方便子类调用

  • 例如这里有个抽象类Animal,里面有个抽象方法eat(),可以看到对于抽象类来说和普通类其实也差不多,这一点上面也说到过,我在里面也提供了构造方法,这是为了子类可以直接方便调用
public abstract class Animal 
    private String name;
    private int age;

    public Animal(String name, int age)
        //提供构造方法,方便子类初始化
        this.name = name;
        this.age = age;
    
    abstract void eat();        //抽象方法

    public String getName() 
        return name;
    

    public int getAge() 
        return age;
    


  • 然后这里有个Rabbit类继承了这个抽象类,并且重写了抽象类里面的抽象方法,因为Rabbit类继承了动物类,所以可以看到直接很方便地调用了父类中的有参构造方法
public class Rabbit extends Animal
    public Rabbit(String name, int age)
    
        super(name, age);
    

    @Override
    void eat() 
        System.out.println(this.getName() + " 不吃窝边草");
    

  • 然后通过测试可以看到继承了抽象类的普通类也很好地实现了对应的功能
public class Test 
    public static void main(String[] args) 
        Rabbit rabbit = new Rabbit("Black Rabbit", 100);
        System.out.println("这只兔子的姓名为:" + rabbit.getName());
        System.out.println("这只兔子的年龄为:" + rabbit.getAge());
        rabbit.eat();
    

3、具体案例

讲了这么多有关abstract关键字的注意事项,现在就让我们到实战中来看看它是具体怎么应用的吧👈

女朋友类(doge)

public abstract class GirlFriend 
    abstract void speak();
    abstract void cooking();


中国女朋友类(doge)

public class ChinaGirlFriend extends GirlFriend
    @Override
    void speak() 
        System.out.println("你好");
    

    @Override
    void cooking() 
        System.out.println("会做水煮鱼");
    

美国女朋友类(doge)

public class AmericanGiralFriend extends GirlFriend
    @Override
    void speak() 
        System.out.println("Hello");
    

    @Override
    void cooking() 
        System.out.println("Can make roast beef");
    


男孩类

public class Boy 
    GirlFriend girlFriend;
    void setGirlFriend(GirlFriend f)
        girlFriend = f;
    
    void showGirlFriend()
        girlFriend.speak();
        girlFriend.cooking();
    


测试类

public class test 
    public static void main(String[] args) 
        GirlFriend girlFriend = new ChinaGirlFriend();
        Boy boy = new Boy();
        System.out.println("中国女朋友");
        boy.setGirlFriend(girlFriend);
        boy.showGirlFriend();

        girlFriend = new AmericanGiralFriend();
        System.out.println("--------------");
        System.out.println("美国女朋友");
        boy.setGirlFriend(girlFriend);
        boy.showGirlFriend();
    

运行结果

  • 好,在看完这段代码后我们来讲解一下,可以看到,首先是创建了一个抽象的女朋友类,然后定义了两个抽象方法,一个是说话,一个是做饭
  • 然后一个中国女朋友类和美国女朋友类分别去继承这个父类,重写里面的方法,然后通过一个男孩类作为一个过渡(doge),设置一个方法去接收两个女孩,形成一个上转型对象,以此来实现多态

四、面向抽象的编程思维

在看完abstract关键字的应用之后,您对多态有没有形成一个概念了呢,其实要实现多态还是要有一个面向抽象的编程思维,这一点是很重要的🔑

  • 我们在企业开发中经常是使用abstract类,其原因是abstract类只关心操作,不关心这些操作的具体实现,可以使程序的设计者把主要精力放在程序的设计上,而不必拘泥于细节的实现(将这些细节交给子类的设计者),这样就可以使得整体项目框架设计者不必把大量的时间和精力花费在具体的算法上
  • 举个很简单的现实生活中的例子,一家出版世界地图的厂家,当设计师在设计一块国家区域的时候,不必去考虑诸如城市中的街道牌号等细节,细节应当由抽象类的非抽象子类去实现,这些子类可给出具体的实例,从而去完成程序功能的具体实现。
  • 所以当我们在设计一个小程序时,可以通过在abstract类中声明若干个abstract方法表明这些方法在整体系统设计中的重要性,方法体的内容细节由它的非abstract子类去完成

———— 以上选段摘自耿祥义《Java2实用教程》

五、多态的经典案例剖析

了解了面向抽象的编程思维,以及看了这么多的有关多态的小案例,接下来就让我们到实战中感受一下多态所带来的魅力吧🍁

1、几何体的体积计算

(1)整体代码展示

//构造一个抽象几何形状类 —— 实现不同子类几何形状面积的求解
public abstract class Gemotrey 
    public abstract double getArea();

//柱类 —— 面向抽象类Gemotrey,为具体底面几何图形提供总抽象类接口
public class Pillar 
    Gemotrey bottom;        //底面几何图形对象
    double height;          //柱体的高

    //传入具体的底面几何图形和柱体的高
    public Pillar(Gemotrey bottom, double height) 
        this.bottom = bottom;
        this.height = height;
    

    //对外获取柱体体积
    public double getVolume()
        if(bottom == null)
            System.out.println("没有底,无法计算面积");
            return -1;
        
        return bottom.getArea() * height;
        //通过具体的几何图形去重写抽象父类的获取面积方法
    


//圆类,继承自抽象类Gemotrey
public class Circle extends Gemotrey
    double r;

    public Circle(double r) 
        this.r = r;
    

    @Override
    public double getArea() 

第三章 Java面向对象(下)

3.1、抽象类

概述:在做子类共性功能抽取时,有些方法在父类中并没有具体的体现,这个时候就需要抽象类了

格式:public abstract class 类名 {}

语法特点:

  1. 抽象类和抽象方法必须使用 abstract 关键字修饰
  2. 抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类
  3. 抽象类不能实例化,要想实例化,参照多态的方式,通过子类对象实例化,这叫抽象类多态
  4. 抽象类的子类,要么重写抽象类中的所有抽象方法,要么子类也是抽象类

内部特点:

成员变量
	既可以是变量
	也可以是常量
构造方法
	空参构造
	有参构造
成员方法
	抽象方法
	普通方法

注意问题:

  1. 与abstract不能共存的关键字:final、private、static
final:被final修饰的类不能有子类,而被abstract修饰的类一定是一个父类
private: 抽象类中的私有的抽象方法,不被子类所知,就无法被复写。而抽象方法出现的就是需要被复写
static:如果static可以修饰抽象方法,那么连对象都省了,直接类名调用就可以了。可是抽象方法运行没意义

3.2、接口

概述:接口就是一种公共的规范标准,只要符合规范标准,大家都可以通用

格式:public interface 接口名 {}

语法特点:

  1. 接口用关键字interface修饰
  2. 类实现接口用implements表示
  3. 接口不能实例化,要想实例化,参照多态的方式,通过子类对象实例化,这叫接口多态
  4. 接口的子类,要么重写接口中的所有抽象方法,要么子类也是抽象类

内部特点:

成员变量
	只能是常量,默认修饰符:public static final
构造方法
	没有,因为接口主要是扩展功能的,而没有具体存在
成员方法
	只能是抽象方法,默认修饰符:public abstract

注意问题:

  1. 抽象类和接口的区别
成员区别:
	抽象类
		变量、常量、构造方法、有抽象方法、也有非抽象方法
	接口
		常量、有抽象方法

关系区别:
	类与类的关系
		继承关系,只能单继承,支持多层继承
	类与接口的关系
		实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口
	接口与接口的关系
		继承关系,可以单继承,也可以多继承

设计理念区别
	抽象类
		对类抽象,包括属性、行为
	接口
		对行为抽象,主要是行为

3.3、内部类

概述:在一个类中定义一个类。举例:在一个类A的内部定义一个类B,类B就被称为内部类

格式:

class Outer {
    public class Inner { }
}

语法特点:

  1. 内部类可以直接访问外部类的成员,包括私有
  2. 外部类要访问内部类的成员,必须创建对象

常见分类:

成员内部类:在成员变量位置定义的内部类
外界访问成员内部类:
	格式:外部类名.内部类名 对象名 = 外部类对象.内部类对象;
	举例:Outer.Inner oi = new Outer().new Inner();

局部内部类:在方法位置定义的内部类
外界访问局部内部类:
    格式:外界是无法直接使用,需要在方法内部创建对象并使用
    注意:局部内部类可以直接访问外部类的成员,也可以访问方法内的局部变量

匿名内部类:匿名内部类其实就是内部类的简写格式,他的前提是继承一个类或者实现接口
    格式:new 类名 ( ) { 重写方法 } 
	     new 接口名 ( ) { 重写方法 }
	本质:是一个继承了该类或者实现了该接口的子类匿名对象

3.4、方法进阶

3.4.1、类作为方法形参和返回值

class Cat {
    public void eat() {
        System.out.println("猫吃鱼");
    }
}

class CatOperator {
    public void useCat(Cat c) {
        c.eat();
    }

    public Cat getCat() {
        return new Cat();
    }
}

public class Main {
    public static void main(String[] args) {
        CatOperator co = new CatOperator();
        Cat c1 = new Cat();
        co.useCat(c1);
        Cat c2 = co.getCat();
        c2.eat();
    }
}

3.4.2、抽象类作为方法形参和返回值

abstract class Animal {
    public abstract void eat();
}

class AnimalOperator {
    public void useAnimal(Animal a) {
        a.eat();
    }

    public Animal getAnimal() {
        return new Cat();
    }
}

class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }
}


public class Main {
    public static void main(String[] args) {
        AnimalOperator ao = new AnimalOperator();
        Animal a1 = new Cat();
        ao.useAnimal(a1);
        Animal a2 = ao.getAnimal();
        a2.eat();
    }
}

3.4.3、接口作为方法形参和返回值

interface Jumpping {
    void jump();
}

class JumppingOperator {
    public void useJumpping(Jumpping j) {
        j.jump();
    }

    public Jumpping getJumpping() {
        return new Cat();
    }
}

class Cat implements Jumpping {
    @Override
    public void jump() {
        System.out.println("猫跳高");
    }
}

public class Main {
    public static void main(String[] args) {
        JumppingOperator jo = new JumppingOperator();
        Jumpping j1 = new Cat();
        jo.useJumpping(j1);
        Jumpping j2 = jo.getJumpping();
        j2.jump();
    }
}

3.4.4、匿名内部类作为方法实参和返回值

interface Jumpping {
    void jump();
}

class JumppingOperator {
    public void useJumpping(Jumpping j) {
        j.jump();
    }

    public Jumpping getJumpping() {
        return new Jumpping() {
            @Override
            public void jump() {
                System.out.println("猫跳高");
            }
        };
    }
}

public class Main {
    public static void main(String[] args) {
        JumppingOperator jo = new JumppingOperator();
        jo.useJumpping(new Jumpping() {
            @Override
            public void jump() {
                System.out.println("猫跳高");
            }
        });
        jo.getJumpping().jump();
    }
}

以上是关于Java | abstract关键字面向对象的第三大特征——多态的主要内容,如果未能解决你的问题,请参考以下文章

Java面向对象 抽象类与抽象方法的使用(关键字abstract)

java 面向对象(二十三):关键字:abstract以及模板方法的设计模式

Java面向对象之抽象类abstract 入门实例

JAVA面向对象 - 抽象类接口

Java面向对象之接口

java面向对象(抽象类)