【Java SE】Java中的继承和多态

一些idea使用小tips:ctrl+鼠标左键,进入方法定义,alt+⬅返回

Java中通过shft+f6修改变量名

Java中判断两个字符串String是否相等使用equal方法

1. 继承

1.1 继承的基本概念

继承(inheritance)机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用。

例如:狗和猫都是动物,那么我们就可以将共性的内容进行抽取,然后采用继承的思想来达到共用

上述图示中,Dog和Cat都继承了Animal类,其中:Animal类称为父类/基类或超类,Dog和Cat可以称为Animal的子类/派生类,继承之后,子类可以复用父类中成员,子类在实现时只需关心自己新增加的成员即可。从继承概念中可以看出继承最大的作用就是:实现代码复用,还有就是来实现多态(后序讲)。

1.2 继承的语法

Object是一个特殊的类,任何一个类都是直接/间接继承自Object类,在Java中如果要表示类之间的继承关系,需要借助extends关键字,具体如下:

修饰符 class 子类 extends 父类 {
// ...
}

举例:父类Animal:

public class Animal {
    private String name;
    private int age;
    public void eat(){
        System.out.println(name + "正在吃饭");
    }
    public void sleep(){
        System.out.println(name + "正在睡觉");
    }
}

子类:

public class Dog extends Animal {
    public void bark(){
        System.out.println(name+"汪汪汪");
    }
}

上面的Dog子类name报错,继承不代表能用得了(某个人从父亲那里继承了一张银行卡,但是他可能不知道密码),private限制了属性在类外不能使用,即使是子类也不能。——>使用set方法

dog.setName("小黄");

注意:

1. 子类会将父类中的成员变量或者成员方法继承到子类中了
2. 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了

1.3 父类成员访问

子类和父类名字相同的情况

public class Animal {
    public String name = "小黄";
    private int age;
    public void eat(){
        System.out.println(name + "正在吃饭");
    }
    public void sleep(){
        System.out.println(name + "正在睡觉");
    }
}
public class Dog extends Animal {

    public String name = "子类中的name";

    public void bark(){
        System.out.println(name + "汪汪汪");
    }

    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.eat();
        dog.sleep();
        dog.bark();

    }
}

语法上就近原则:来自与父类的方法,打印的属性就一定是父类中的属性,子类中的方法优先读取子类的name(一般不建议这么写,父类属性和子类属性不要写成一样的)

super关键字

由于设计不好,或者因场景需要,子类和父类中可能会存在相同名称的成员,如果要在子类方法中访问父类同名成员时,该如何操作?直接访问是无法做到的,Java提供了super关键字,该关键字主要作用:在子类方法中访问父类的成员

如果出现父类属性和子类属性同名,this访问子类(当前类的属性),super访问父类属性,如下:

 public void bark(){
        // this.name访问子类的属性
        System.out.println(this.name + "汪汪汪");
        //super.name访问父类的属性
        System.out.println(super.name + "汪汪汪");
 }

注意:同this一样,super不能在静态方法中使用,super也能调用方法,super还能用来调用父类的构造方法。(构造子类对象时,要先构造父类对象)

public Animal(String name){
        this.name = name;
        System.out.println("父类构造方法");
}
public Dog(){
        super("小黄");
        System.out.println("子类构造方法");
}

 1.4 protected关键字

在类和对象章节中,为了实现封装特性,Java中引入了访问限定符,主要限定:类或者类中成员能否在类外或者其他包中被访问。protected关键字,和public private并列关系(还有个系统默认的default)

注意:实际情况中,绝大部分都是public和private。

1.5 继承方式

1.6 final关键字

final关键可以用来修饰变量、成员方法以及类。

①修饰变量或字段,表示常量(即不能修改),类似于C语言的const。

②修饰类:表示此类不能被继承。

③修饰方法,该方法不能被子类重写

1.7 继承与组合

和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段。

继承表示对象之间是is-a的关系,比如:狗是动物,猫是动物

组合表示对象之间是has-a的关系,比如:汽车

class Head{
    //猫的头
}
class Tail{
    //猫的尾巴
}
class Foot{
    //猫的爪子
}
public class Cat {
    private Head head;
    private Tail tail;
    private Foot foot;
}

1.8 继承关系上代码块等的初始化顺序

静态代码块

就地初始化                -——————>   引入继承后,先父类再子类

构造代码块                -——————>   先静态,后实例,先父类后子类

构造方法

package object2.package3;

public class Parent {
    static{
        System.out.println("parent 静态代码块");
    }

    {
        System.out.println("Parent 构造代码块");
    }
     public Parent(){
         System.out.println("Parent 构造方法");
     }
}
public class Child extends Parent{
    static{
        System.out.println("子类静态代码块");
    }

    {
        System.out.println("子类构造代码块");
    }

    public  Child(){
        System.out.println("子类构造方法");
    }

    public static void main(String[] args) {
        Child child = new Child();

    }
}

2. 多态

2.1 多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。(一个对象或引用,具备多种形态,结合上下文来看是哪种形态)

public class Son {
    public void eat(){
        System.out.println("Son在吃东西");
    }
}
public class Xiaotang extends Son{
    public void eat(){
        System.out.println("小汤在吃米饭");
    }
}
public class Yuanyuan extends Son{
    public void eat(){
        System.out.println("圆圆在吃谷子");
    }
}
public class Test {
    public static void main(String[] args) {
        Son son = new Son();
        son.eat();

        son = new Xiaotang();
        son.eat();

        son = new Yuanyuan();
        son.eat();
    }
}

Son是父类,有两个方法对他它进行了重写,son就相当于是一个引用,使用父类的引用指向子类的实例,这在语法上是完全可以的,也称为向上转型。

上面代码,表面上看,都是son.eat(),但是执行过程中,执行了完全不同的逻辑~~具体son.eat()执行的是哪个逻辑得结合上下文,明确“son到底指的是谁"。

上面三段代码,为类的实现者,下面Test方法为类的使用者,类的使用者和类的实现者不是同一人,让类的使用者降低使用的负担.不需要了解类的实现细节。

在java中要实现多态,必须要满足如下几个条件,缺一不可:
1. 必须在继承体系下
2. 子类必须要对父类中方法进行重写
3. 通过父类的引用调用重写的方法

2.2 重写

重写也叫override,是指父类有一个方法,子类提供了一个同样的方法(方法名字,参数列表,返回值类型相容)相容指1. 类型完全相同 2. 子类重写的方法的返回值类型是父类对应方法返回值类型的子类

方法重写(override)和方法重载(overload)不同,方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

访问权限:private < default< protected < public,子类的方法权限要>=父类的方法权限,如父类方法为private,也不能重写

【重写的设计原则】开闭原则,对于修改关闭,对于扩展开放
对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。
例如:若干年前的手机,只能打电话,发短信,来电显示只能显示号码,而今天的手机在来电显示的时候,不仅仅可以显示号码,还可以显示头像,地区等。在这个过程当中,我们不应该在原来老的类上进行修改,因为原来的类,可能还在有用户使用,正确做法是:新建一个新手机的类,对来电显示这个方法重写就好了,这样就达到了我们当今的需求了

son.eat();

当代码执行到上面语句时,JVM就会在运行过程中,分析son真实指向的对象是啥类型,进一步的调用匹配的类型的方法。====》这个过程也称为动态绑定,eat执行哪个版本是程序运行时确定的,不是编译时确定的。

而多态就是基于动态绑定的机制来实现的,语法层面叫多态,而这是JVM内部的实现原理。

2.3 向上转型和向下转型

向上转型

向上转型有三种:

1. 直接赋值

Animal cat = new Cat();

2.方法传参,形参为父类引用,可以接受任意子类的对象

public static void eatFood(Animal animal){
        animal.eat();
}
eatFood(new Dog());

3. 作返回值,可以返回任意的子类对象

public static Animal fun(){
        return new Cat();
}

上面三种方法本质上都是赋值

向下转型

将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。

//向下转型,需要先创建一个父类引用,这个引用指向子类实例
Dog dog = (Dog) animal;
//向下转型依赖于向上转型,相当于把向上转型的引用转回来
dog.bark();

向下转型的时候,需要确保,当前这里的父类引用确实是指向子类实例的.如果不是就算强转了,也无法调用里面的方法,这时候需要一个关键字instanceof来做类型判断,判断某个引用是否指向某个类型的实例

Animal animal = new Animal();
animal.eat();
if(animal instanceof Dog){
    Dog dog  = (Dog) animal;
    dog.bark();
}else{
    System.out.println("类型出错");
}

2.4 多态的优缺点

能够降低代码的 "圈复杂度", 避免使用大量的 if - else

什么叫 "圈复杂度" ?
圈复杂度是一种描述一段代码复杂程度的方式。一段代码如果平铺直叙, 那么就比较简单容易理解。 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂。因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度"。如果一个方法的圈复杂度太高,就需要考虑重构。不同公司对于代码的圈复杂度的规范不一样, 一般不会超过 10 。

假如有以下代码:

public class Shape {
    public void draw(){
        System.out.println("画一个形状");
    }
}
public class Rect extends Shape{
    public void draw(){
        System.out.println("♦");
    }
}
public class Flower extends Shape{
    public void draw(){
        System.out.println("❀");
    }
}
public class Circle extends Shape{
    public void draw(){
        System.out.println("⭕");
    }
}

如果我们要打印多个形状,不基于多态的实现,就会有很多的条件判断语句

public class Test {
    public static void main(String[] args) {
        Circle circle = new Circle();
        Rect rect = new Rect();
        Flower flower = new Flower();
        String[] shapes = {"Circle","Rect","Rect","Flower"};
        for(String shape : shapes){
            if(shape.equals("Circle")){
                circle.draw();
            }else if(shape.equals("Rect")){
                rect.draw();
            }else if (shape.equals("Flower")){
                flower.draw();
            }
        }
    }
}

若使用多态,此处Circle等类型是Shape的子类,子类的引用是可以放到父类的数组中(向上转型

Shape[] shapes = {new Circle(),new Rect(),new Rect(),new Flower()};
for(Shape shape : shapes){
    shape.draw();
}

可扩展能力强,如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低

public class Triangle extends Shape{
    public void draw(){
        System.out.println("▲");
    }
}
Shape[] shapes = {new Circle(),new Rect(),new Rect(),new Flower(), new Triangle()};

对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.
而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高.

2.5 避免在构造方法中调用重写的方法

多态是好东西,但是不要在父类的构造方法中触发,父类构造方法执行的时候,子类的属性还没初始化,此时执行子类的方法,使用的子类的属性,就属于是"未初始化"的属性~~

public class Parent {
    public Parent(){
        fun();
    }
    public void fun(){
        System.out.println("Parent.fun");
    }
}
public class Child extends Parent{
    public int num = 10;
    @Override
    public void fun() {
        System.out.println("Child.fun, num = " + num);
    }

    public static void main(String[] args) {
        Child child = new Child();
    }
}

new child会在执行子类的构造方法之前先执行父类的构造方法,而父类的构造方法会调用fun方法,这里的调用引发了多态,实际调用了子类的fun版本,而子类child的属性还没初始化(哪怕就地初始化也没执行),此处的0是JVM申请内存时自动填充的0。

结论: "用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值