Java知识点总结【5】继承组合多态

面向对象的特性:类和对象,抽象,封装,继承,组合,多态,反射...其中,“三大特性”是指封装,继承和多态。下面我们对于继承、组合和多态进行总结~

继承

1.什么是继承

代码中如果出现“重复代码”往往意味着一定的风险,当我们要修改这段“重复代码”时,可能要修改多处,造成代码的维护性下降。

为了避免这件事情,可以使用面向对象中的一个重要用法——继承

继承的目的是代码重用,类的重用。把多个类之间的共同代码(共同特性)提取出来,放到"父类"中,然后再由各个子类分别继承这个父类,子类会拥有父类的属性和方法,从而就可以把重复代码消灭了。使用的关键字是extends

被继承的称为父类/基类/超类,继承的类称为子类/派生类。

2.示例  Animal Cat Bird

①我们发现,小猫和小鸟都有共同的特性:名字,吃东西。那么就可以将这些特性写在一个类里面,表示父类。如下:

class Animal{
    String name;
    public Animal(String name){
        this.name=name;
        System.out.println("Animal的构造方法");
    }
    public void eat(){
        System.out.println(this.name+"正在吃");
    }
}

②Cat可以继承自Animal,这样不用重复定义就拥有了name属性和eat()方法。针对小猫的其他特点,也可以继续添加其他的功能,比如小猫可以跳jump~

class Cat extends Animal{
    public Cat(String name){
        super(name);  //必须放到第一行
        System.out.println("Cat的构造方法");
    }
    public void jump(){
        System.out.println(this.name+"正在跳");
    }
}

③Bird基于Animal,添加了飞的功能fly~

class Bird extends Animal{
    public Bird(String name){
        super(name);  //必须放到第一行
        System.out.println("Bird的构造方法");
    }
    public void fly(){
        System.out.println(this.name+"正在飞");
    }
}

④下面我们来测试一下,是否new一个子类实例,拥有父类的功能

public class Main{
    public static void main(String[] args) {
        Cat cat=new Cat("小狸花");
        cat.eat();
        cat.jump();
        Bird bird=new Bird("小鸟");
        bird.eat();
        bird.fly();
    }
}

运行结果

分析:我们分别创建了Cat和Bird的实例,可以发现,在这两个类中并没有写eat()这个方法,只是在父类Animal中定义了,但是我们能够成功调用,所以通过继承的确是可以拥有父类的功能~

但是有一个问题,我们从结果中可以看到,在创建Cat实例的时候,首先调用了父类Animal的构造方法,其次是子类Cat的构造方法。实际上,当我们去new一个Cat实例的时候,就会先创建一个Animal实例(在Cat的内部),也就是构造子类实例的时候,会在子类实例的内部自动构造一个父类的实例(如下图),构造方法是在创建实例时调用的,那这下也就不难理解了~

细心的小伙伴可能也会发现,上面子类的构造方法中有super这个关键字,使用super关键字就能获取到父类的实例的引用,获取子类实例的引用是用this关键字。

注意:

每个类都有构造方法,如果我们不显示创建构造方法,那么编译器就会给这个类自动生生成一个没有参数的构造方法。

当父类里面没有写构造方法的时候,就被自动生成了没有参数版本的构造方法。此时如果new子类实例,就会调用到刚才父类这个没参数版本的构造方法。

当父类里有构造方法的时候,并且这个构造方法带有参数的时候,编译器就不再自定义生成无参数版本的构造方法了。此时再创建子类实例,就需要显示的调用父类的构造方法,并且进行传参,否则创建不出来父类的实例,就会编译出错。修改这个问题也很简单,只要在子类的构造方法中,显示调用父类的构造方法即可(使用super关键字)。

3.其他注意点

Java中的继承是单继承,一个类只能有一个父类,但是这个父类还可以继承自其他的类。

子类会继承父类所有的属性和方法,无论是public还是private。只不过,private修饰的成员是在子类中无法访问的。(被private修饰只是在类的内部才能访问到,哪怕是自己的子类也不行)

final修饰一个变量,表示常量(不能修改);final修饰类,表示被修饰的类禁止被继承,防止继承被滥用。


组合

1.什么是组合

组合,也是为了代码重用,一个类的成员也可以是其他的类的实例  

通过了解了继承,以上面的例子为例:小猫是动物,小鸟是动物,可以将继承的语义理解为is-a

组合的语义可以理解为:has-a。比如我们我们以学校为例,学校里面包含若干学生和老师。

2.示例

①学校包含两个学生和两个老师

class Teacher{
    String name;
    String subject;
}
class Student{
    String name;
    String num;
}
public class School{
    public Teacher[] teacher=new Teacher[2];
    public Student[] student=new Student[2];
}

②圆中包含原点和半径

class Point{
    
}
class Raduis{
    
}
public class Circle {
    Point point=new Point();
    Raduis raduis=new Raduis();
}

多态

多态中有三种重要的语法基础:向上转型、动态绑定、重写。缺一不可~

1.向上转型

父类的引用指向了一个子类对象(看起来好像把子类的引用转成了父类的引用)。有三种发生的时机:直接赋值、方法传参、方法返回。下面进行举例分析。

例①直接赋值

class Animal{
    
}
class Cat extends Animal{
    
}
public class Pra {
    Animal animal=new Cat();  //直接赋值时发生向上转型
}

例②方法传参时

//Animal和Cat两个类和例①中的一样,就不再写了
//......
public class Pra {
    public static void main(String[] args) {
        func(new Cat());
    }

    public static void func(Animal animal) {  //传参时发生向上转型

    }
}

注意观察调用func函数时传的实参是Cat的实例的,形参是Animal类型。

例③方法返回时

public static Animal func() {
    return new Cat();  //方法返回时发生向上转型
}

func函数的返回值是Animal类型,返回语句返回的是Cat的实例

注意:父类的引用只能访问到父类中的属性和方法,访问不到子类独有的属性和方法。

引用我们可以理解为一个低配的指针,里面保存的是地址。

2.动态绑定

如果父类中包含的方法在子类中有对应的同名同参数的方法,就会进行动态绑定。由运行时决定调用哪个方法。

一般动态/静态分别指的是编译时/运行时,和static无关。

示例

class Animal{
    public void eat(String food){
        System.out.println("小动物正在吃"+food);
    }
}
class Cat extends Animal{
    public void eat(String food){
        System.out.println("小猫正在吃"+food);
    }

}
public class Pra {
    public static void main(String[] args) {
        Animal animal=new Cat();
        animal.eat("小鱼干");
    }
}

运行结果

分析:

我们的父类Animal中和子类Cat中,有一个同名同参数的方法eat(),此时涉及到了动态绑定,在程序运行时,看animal究竟是指向一个父类的实例还是指向一个子类的实例。指向父类版本的实例,就执行父类版本的eat,指向子类版本的实例,就执行子类版本的eat。animal这个引用指向的是父类的实例还是子类的实例,是运行时才能确定的。

3.重写

其实呢,重写和上面的动态绑定本质上是相同的事情,只不过所看待的角度不同,动态绑定,我们是站在编译器和JVM实现者的角度来看,而方法重写则是站在Java的语法层次上来看的。也就是说方法重写是 Java 语法层次上的规则, 而动态绑定是方法重写这个语法规则的底层实现.。

重写是父类和子类之间,存在同名的方法,参数相同,此时通过父类的引用调用该方法,就会触发重写,此时具体执行哪个版本的方法由动态绑定规则来决定。

在代码中,我们得显示的告诉编译器当前这个子类的方法是重写了父类的方法,这样的话,编译器就能够更好地进行检查和校验工作。加上@Override这个注解即可。

4.多态举例

比如说,新的产品应该兼容旧版本产品的功能,在这个基础上再添加新的功能。尤其是在使用别人开发的类的时候,这种思路更加常见。

下面我们来举例说明。

①父类Shape

public class Shape {
    public void draw(){

    }
}

②子类Circle

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

③子类Rect

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

④子类Flower

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

⑤主类Main

public class Main {
    public static void main(String[] args) {
//        Shape shape1=new Circle();
//        Shape shape2=new Rect();
//        Shape shape3=new Flower();
//        drawShape(shape1);
//        drawShape(shape2);
//        drawShape(shape3);
        Shape[] shapes={new Circle(),new Rect(),new Flower()};
        for(Shape x:shapes){
            drawShape(x);
        }
    }
    public static void drawShape(Shape shape){
        shape.draw();
    }
}

运行结果

分析:多态是一种程序设计的思想方法,具体的语法体现:向上转型,方法重写,动态绑定。多态直观的理解,“一个引用,对应到多种形态(不同类型的实例)”。

Shape[] shapes={new Circle()...}在这里体现了多态中的向上转型。在drawShape()方法中,shape.draw()体现了动态绑定,具体调用的是Shape中的draw方法,还是子类中的draw方法,这是运行时才能决定的。在Circle,Rect,Flower中都有一个方法和父类中的方法同名同参数(draw),体现了方法重写

实现drawShape方法的人和实现Shape,Cicle...这几个类的人可能不是同一个人。假如,这个drawShape方法是让我们自己来实现,功能是调用shape的draw方法,那么就直接在方法里面写上shape.draw();就行了,而不用管到底这个shape指向的是circle类型,flower类型啥的,不需要再去判断,如果shape指向的是圆的实例就调用圆的draw,指向花就去调用花的draw,这些是我不用去考虑的,我只需要写一句就可以了。

多态的好处:

  • 第一个好处:多态这种设计思想,本质上是“封装”的更近一步。封装的目的是为了让类的使用者不需要知道类的实现细节,就能使用,但是使用者仍然需要知道这个类是啥类型;使用多态的话,此时类的使用者,不仅不需要知道类的实现细节,也不需要知道这个类具体是啥类型,只要知道这个类有一个draw方法就可以了,这个时候类的使用者知道的信息更少,使用的成本更低。
  • 第二个好处:方便扩展。未来如果需要新增新的子类,对于子类的使用者来说影响很小。
  • 第三个好处:消灭一些分支语句,降低程序的圈复杂度。

5.向下转型

把父类的引用转成子类的引用。

1)父类:Animal,子类:Cat,Bird。下面进行举例:

例①

Animal animal=new Cat();
Cat cat=(Cat)animal;  //向下转型

分析:上面代码是正确的。向下转型必须确保,animal指向的确定是一个Cat类型的实例,才可以进行转换,否则转换可能失效。

例②

Animal animal2=new Bird();
Cat cat2=(Cat)animal2; //这是无效的

分析:上面的代码转型是无效的。向下转型是存在限制的,不能随便转,其实就相当于向上转型的逆过程通过向上转型得到的父类的引用,借助向下转型还原回原来的类型。

然而我们此处,先是将Bird实例的引用转化为Animal父类的引用animal2,然后试图将animal转化为Cat类型,虽然编译可以通过,但是是无效的。

例③

Animal animal3=new Animal();
Cat cat3=(Cat)animal3;  //这是无效的

分析:虽然编译可以通过,但是无效。

2)向下转型的应用场景(如在数据库中):

有些方法只是在子类中存在,但是父类中不存在。此时使用多态的方式就无法执行到对应的子类的方法了,就必须把父类的引用先转回成子类的引用(向下转型),然后再调用对应的方法。

可以在向下转型之前,先判定当前的父类的引用到底是不是指向该子类,如果不是就不进行向下转型。使用instanceof(作用:比较类型)判断,这样就避免出现上面例②例③无效的情况~

class Shape {
    public void draw(){

    }
}

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

public class Main {
    public static void main(String[] args) {
        Shape shape=new Flower();
        if(shape instanceof Flower){  //instanceof判断shape所指向的和Flower是不是同类型
            Flower flower=(Flower)shape;
            flower.draw();
        }
    }
    public static void drawShape(Shape shape){
        shape.draw();
    }
}

运行结果

分析:

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


写了好久总算是写完了,但是感觉写的还不是很到位,还得慢慢更正,欢迎各位大佬指点~~~

猜你喜欢

转载自blog.csdn.net/weixin_43939602/article/details/112987258
今日推荐