简单易懂且详细!用Java带你搞懂面向对象的三大特征:封装、继承、多态!

什么是面向对象? 

简单来说就是:依靠对象之间相互完成一件事情

用洗衣服来举例

我们洗衣服只需要把衣服丢进洗衣机加入洗衣粉按开关就完成了洗衣服这件事情

完成洗衣服这件事情就只有四个对象:洗衣机衣服洗衣粉

关于洗衣机是怎么洗衣服的,怎么甩干衣服的我们并不需要去关注

我们只需要去关注洗衣服这件事情所有的对象是谁,以及如何完成这件事情

这就是面向对象

面向对象的三大特征分别是封装、继承、多态

这篇文章用代码举例来深入理解这面向对象编程的三大特征


1.封装

1.1 什么是封装? 

简单来说就是屏蔽细节,对外提供接口

拿电脑来举例:电脑内部有许多元器件,比如内存条,CPU,显卡等等

但是用户并不需要关注他们是怎么运行的

用户只需要用键盘和鼠标来完成他们想要做的事情

这就是封装

将内部的细节封装起来,然后对用户提供可以操作的接口

1.2 用代码举例

我们用代码来举一个例子

//Computer类

package Computer;

public class Computer {
    private String compuerbrand;
    private String gpu;
    private String momery;

    public Computer(String compuerbrand,String gpu,String momery){
        this.compuerbrand = compuerbrand;
        this.gpu = gpu;
        this.momery = momery;
    }

    public void PowerOn(){
        System.out.println(compuerbrand+"电脑开机!");
    }

    public void ComputerGame(){
        System.out.println("使用"+gpu+"玩游戏!");
    }

    public void PowerOff(){
        System.out.println(compuerbrand+"电脑关机!");
    }
}

//TestComputer这个类

package Computer;

public class TestComputer {
    public static void main(String[] args) {
        Computer computer1 = new Computer("Lenovo","RTX3060","512GB");
        computer1.PowerOn();
        computer1.ComputerGame();
    }
}

我们先创建一个包命名为Computer

Computer这个类中我们定义好我们电脑的属性和功能

TestComputer这个类中我们输入我们的参数进去

直接调用我们的功能会发现我们的参数也在里面

1.3 代码解读

我们再详细的谈谈这段代码

假设Computer是一台真的电脑,而TestComputer是用户的话

这段代码意味着:我们用户拿到一台我们想要的配置的电脑

Computer computer1 = new Computer("Lenovo","RTX3060","512GB")

这段代码代表着:这台电脑有这些功能

public void PowerOn(){
        System.out.println(compuerbrand+"电脑开机!");
    }

    public void ComputerGame(){
        System.out.println("使用"+gpu+"玩游戏!");
    }

    public void PowerOff(){
        System.out.println(compuerbrand+"电脑关机!");
    }

这段代码是:调用电脑的功能

computer1.PowerOn();
computer1.ComputerGame();

在TestComputer类中我们作为用户

我们只需要调用这些方法就能实现我们想要的功能

并不需要去理会Computer类的内部结构是怎样的

我们将细节隐藏起来并且对外提供接口来操控

这就是封装!


2.继承

2.1 什么是继承?

谈完了封装我们再来谈谈继承

在面向对象里面,继承指的是抽取共性,从而实现代码的复用

2.2 用代码解释继承

class Dog {
    public boolean Smart;
    public String name;
    public int age;
    public float weight;


    public void Eat() {
        System.out.println(name + "正在吃饭!");
    }

    public void Sleep() {
        System.out.println(name + "正在睡觉!");
    }
}

class Cat {
    public boolean angry;
    public String name;
    public int age;
    public float weight;


    public void Eat() {
        System.out.println(name + "正在吃饭!");
    }

    public void Sleep() {
        System.out.println(name + "正在睡觉!");
    }
}

假如说我们定义了两个类,一个Cat,一个Dog

我们会发现

他们之间存在着许多的相同之处

如果这个时候我们就可以把他们的相同之处抽取出来放到Animal1这个类中

class Animal{
    public String name;
    public int age;
    public float weight;



    public void Eat(){
        System.out.println(name+"正在吃饭!");
    }

    public void Sleep(){
        System.out.println(name+"正在睡觉!");

    }
}

通过继承的方法

就使得猫和狗这两个类也能使用到 Aniaml 当中的属性了

那么最终的效果就是这样的

class Animal1{
    public String name;
    public int age;
    public float weight;



    public void Eat(){
        System.out.println(name+"正在吃饭!");
    }

    public void Sleep(){
        System.out.println(name+"正在睡觉!");

    }
}

class Dog extends Animal1{
    public boolean Smart;
}

class Cat extends Animal1 {
    public boolean angry;
    }

我们用画图来解释就是这样的

同时

我们将被继承的类称为父类,而继承他的类叫做子类 

所以继承要解决的问题就是:共性抽取,实现代码复用!

2.2 父类和子类

当我们在父类和子类里面拥有相同名称的成员变量和成员方法时

子类的调用的顺序是“就近原则”

成员变量和方法名称相同子类优先调用自己的成员变量和方法

当自己没有这些成员变量和方法时再调用父类的成员变量和方法

如果两者都没有那么编译就会报错

我们用代码来演示一下

class Animal1{
    public String name;
    public int age = 3;
    public float weight;



    public void Eat(){
        System.out.println(name+"正在吃饭!");
    }

    public void Sleep(){
        System.out.println(name+"正在睡觉!");

    }
}

class Dog extends Animal1{
    public boolean Smart;
    

    public void showName(){
        name = "hello";
        System.out.println(name+"今年"+age+"岁了!");
        Eat();
    }

    public static void main(String[] args) {
        Dog dog1 = new Dog();
        dog1.showName();
    }

}

运行结果如下:

 2.3 实例化顺序

当我们在类里面继承父类的时候我们会发现一个问题

当我们直接给父类的变量赋值时会发生编译错误

而当我们在自己定义的方法里面给变量赋值时却不会发生任何的错误 ---> 编译通过

 原因就在于实例化顺序

子类在实例化(即调用构造函数)之前会先初始化成员变量

所以这里子类中的name就是未定义其类型,而不是父类中的那个name

子类实例化的时候会先调用父类默认的无参构造函数,这时候才能从父类中继承得到其成员变量

所以要在子类实例化以后才能引用继承得来的成员变量

也就是说我们的name放进方法里面不会发生编译错误

是因为编译器认为只有实例化了才能调用这个方法,所以name放进去编译通过

2.4 super关键字

上面我们说到了

子类的调用的顺序是“就近原则” 

当成员变量名称相同的时候子类优先调用自己的成员变量和方法

那么我们就是要调用相同名称的父类成员变量和方法那么要怎么办呢?

这时候就要用到super这个关键字了

我们用代码来演示一遍

class Animal{
    public String name = "World";
}

class Dog extends Animal1{

    public String name = "hello";

    public void showName(){
        System.out.println(super.name+"今年"+age+"岁了!");
    }
}

 假如我们有两个类一个animal,一个dog

这个时候我们调用了super关键字就可以访问到Aniaml1中name

运行结果如下

 

super是子类从父类继承下来的部分成员变量或者方法的引用!而不是父类的引用!

这是我们需要重点注意的

2.5 super调用构造方法

我们还是用代码来发现问题,解决问题

我们正常的为两个类来写构造方法

但是我们惊讶的发现Dog这个类里面的构造函数竟然编译不通过!发生编译错误!

这是为什么?

原因就在于我们的父类的构造方法是带参数的构造方法

子类由两个部分组成继承下来的部分和自身新的部分

所以我们在构造子类的对象的时候首先调用父类的构造方法

如果是带参数的构造方法那么我们就要使用super传参,帮助父类完成构造

如果是不带参数的构造方法那么我们就不用调用super关键字来帮助父类完成构造

因为系统自动帮助我们提供一个不带参数的super来帮助父类完成构造了

在这些完成之后我们再调用子类的构造方法完成构造

刚刚我们在讲到实例化顺序也是这个道理

所以我们用将代码修改一下

当我们把参数传进去以后就不会报错了

2.6 代码块与构造方法

我们再来做一个实验

当我们编写静态代码块、实例代码块、构造方法后哪个是最先运行的?

public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("构造方法执行");
        }

        {
        System.out.println("实例代码块执行");
        }

        static {
        System.out.println("静态代码块执行");
        }
}
class Student extends Person{
    public Student(String name,int age) {
        super(name,age);
        System.out.println("Student:构造方法执行");
    } {
        System.out.println("Student:实例代码块执行");
    }
    static {
        System.out.println("Student:静态代码块执行");
    }
}
public class Test3 {
    public static void main(String[] args) {
        Student student = new Student("littleKom",8);
    }
}

我们用这段代码就可以进行我们的实验

实验结果如下

显然还是遵循:先执行静态代码块,再执行实例代码块,最后再执行构造方法的原则


3.多态

在了解多态之前我们必须要知道两个

3.1重写

3.1.1 基本介绍

重写顾名思义就是重新写,那么在Java当中重写是这样定义的:

重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程,进行重新编写,返回值和形参都不能改变即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。

 3.1.1.1 重写与构造方法

class A{

    public A(){
        func();
    }
    
    public void func(){
        System.out.println("A::func");
    }
}
class C extends A {
    private int num = 1;
    
    @Override
    public void func() {
        System.out.println("C.func() " + num);
    }
}

public class Test5 {
    public static void main(String[] args) {
        C c =new C();
    }
}

我们在这里定义了两个类,这个时候我们在C里面重写了构造方法

按照实例化顺序,我期望的运行结果是先输出A::func再输出C.func

结果却是

 这是为什么

后来我查询了资料才发现

原来构造 C 对象的同时, 确实是会调用 A 的构造方法.

但是A 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 C 中的 func方法

由于此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态,值为 0

所以在构造函数内,尽量避免使用实例方法,除了final和private方法

否则就会造成一些极其困难发现且解决的方法

3.1.2 代码演示

我们通过代码来演示一遍

class Person{
    public String name = "Kom";
    public int age;
    public String gender;
    
    public void eat(){
        System.out.println("正在吃饭!");
    }
}
class Student extends Person{
    @Override
    public void eat() {
        System.out.println(name+"正在吃午饭!");
    }
}

然后我们运行一下可以得到运行结果:

3.1.3 代码解释 

我们在父类里面定义了一个eat 的方法

我们在子类继承了父类

当我们再次用到eat这个方法时

我们可以对里面的内容进行重写

根据我们自身的需求来编写我们想要的功能

这就是重写

3.1.4 注意事项

1.子类在重写时,一般必须与父类方法原型一致:返回值类型/方法名/参数列表要完全一致

2.当然也有特殊情况,比如返回值可以不同,但必须要是父子关系

我们用代码来解释一下这句话

 

当我们将返回值类型修改了以后我们可以发现这样的代码是可以通过编译的

3.如果父类方法被public修饰,则子类中重写该方法就不能声明为protected

那么也就是说:访问权限不能比父类中被重写的方法的访问权限更低

4.父类被static、private修饰的方法、构造方法都不能被重写。

5.重写的@Override的作用在于帮助我们检查重写方法是否正确

如果我们在上面的例子中将方法名称修改成ate,那么@Override就会检查出来父类没有这个方法

编译就不会通过,发生编译错误

3.2向上转型

3.2.1 定义

向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。

语法格式:父类类型 对象名 = new 子类类型()

我们假设Animal是父类,Cat是子类,那么我们编写代码就应该这样子编写

Animal animal = new Cat();

3.2.2 动态绑定

我们还是通过代码来发现问题

class Animal{
    public String name;
    public int age;

    Animal(String name,int age){
        this.name = name;
        this.age = age;
    }

    public void eat(){
        System.out.println("Animal::eat");
    }
}
class Dog extends Animal{

    Dog(){
        super("Hello",3);
    }
    public void eat(){
        System.out.println("Dog::eat");
    }
}
public class Test2 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Animal animal = dog;
        animal.eat();
    }
}

 最终的运行结果

 我们来解释一下这段代码:

我们首先先创建了两个类一个Animal,一个Dog

我们在主函数里面首先实例化了Dog这个类

然后我们创建了 Animal 并将 animal 引用 dog 这个对象

我们打印了eat这个方法最终会发现我们调用不是animal自身的eat方法

而是dog里面的eat方法

这是因为动态绑定的缘故:

即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法

这就是动态绑定

3.2.3 用代码实现向上转型

    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.name = "Hello";
    }

我们只需要将上面的代码修改一下就可以实现向上转型了 

向上转型的有点在于:简化代码

但缺点就在于不能调用子类特有的属性

 假如我们在上面的代码中给狗添加两个特有的成员变量的方法

那么这个时候会发生语法错误

这也就是说向上转型中无法调用子类特有属性的缺点

3.3多态的实现

刚刚我们说的是直接赋值类型的向上转型

那么还有作为传参类型的向上转型

我们用代码演示一下

class Animal{
    public String name;
    public int age;

    public Animal(String name,int age){
        this.name = name;
        this.age = age;
    }

    public void eat(){
        System.out.println("Animal::eat");
    }
}
class Dog extends Animal{
    public String nameDog;
    public String name;
    public int age;
    Dog(String name, int age){
        super("Hello",3);
        this.name =name;
        this.age= age;
    }
    public void eat(){
        System.out.println("Dog::eat");
    }
}
class Cat extends Animal {
    public String name;
    public int age;

    public Cat(String haha, int i) {
        super("Hello",3);
        this.name = name;
        this.age= age;
    }

    public void eat(){
        System.out.println("Cat::eat");
    }
}

public class Test2 {
    public static void function2(Animal animal) {
        animal.eat();
    }
    
    public static void main(String[] args) {
        Dog dog = new Dog("hello",10);
        function2(dog);

        Cat cat = new Cat("haha",7);
        function2(cat);

    }
}

运行结果如下:

 

在这段代码中我们用Animal animal作为参数,用dog或者cat传参传进去

从而实现了向上转型

同时我们也展现出来了多态

都是相同的function但是却展现了不同的结果

这就是多态!

多态的好处就在于可以让代码更加的简洁

比如说我们有这段代码

class Shape {
    public void draw() {
        System.out.println("画图形!");
    }
}
class Rect extends Shape{
    @Override
    public void draw() {
        System.out.println("♦");
    }
}
class Cycle extends Shape{
    @Override
    public void draw() {
        System.out.println("●");
    }
}
class Flower extends Shape{
    @Override
    public void draw() {
        System.out.println("❀");
    }
}

我们想要打迎出来这些符号

那么只需要以下简单的几行代码就可以实现

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

运行结果如下:

 

如果我们使用 if- else 语句来编写代码,那么要写的代码就十分的多了

这就是多态的好处:简化代码!降低圈复杂度!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值