面向对象的三大特征(封装、继承、多态)

  • Post author:
  • Post category:其他



Java知识点总结:想看的可以从这里进入



3、 面向对象三大特征


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



3.1、封装

封装是面向对象编程的核心思想,简单点说就是,我把某些东西封装起来,这些关键的核心的东西不能给你看,但是我可以提供给你一些简单使用的方法。

就说现在最最常用的手机,人人都会用,打电话、发短信、玩游戏、刷视频等等,但你知道手机怎么实现这些功能的吗??不知道吧,我们会用就可以了,怎么实现的对我们来说不重要。那这就很类似封装的理念了。


封装其实是对外隐藏复杂细节,提供简单易用的接口,便于外界调用,从而提高系统的可扩展性、可维护性。在Java中这种隐藏或公开是通过权限修饰符来实现的。

封装

Java中类就是对具体事物的一种封装,类中的方法等等也是一种封装。我们把数据、一系列操作数据的函数封装到方法中,然后通过权限修饰符控制哪些方法可以让外知道,哪些只能自己知道,这样就能减少核心的数据被外界获取甚至破坏。

我们最初学习Java时,往往都是把代码直接写在main方法中的,但是随着学习的深入,遇到的逻辑越来越复杂,我们发现只靠main方法是不能满足全部需要的,这时候,我们开始在类中扩展其他方法,最后通过main方法调用运行。再后来我们逐渐开始去写不同的类,甚至不同的业务模块。这时候就会发现,一个简单的封装能带来多大的好处。

  • 通过封装,我们可以保护代码被破坏,提高数据安全性。

    使用者只能通过事先定制好的方法来访问数据,可以方便地加入控制逻辑限制对属性的不合理操作。

  • 通过封装,我们提高了代码的复用性(有些方法、类在很多地方都能多次反复使用)

  • 通过封装,带来的高内聚和低耦合,使用不同对象、不同模块之间能更好的协同,同时便于修改,增强代码的可维护性

    • 高内聚 :类的内部数据操作细节自己完成,不允许外部干涉;
    • 低耦合 :仅对外暴露少量的方法用于使用

比如说一个类最简单的封装就是把属性隐藏起来,只提供 get和set 方法进行操作:

public class Student {
    //姓名
    private String name;
    //年龄
    private int age;

    //get方法获取年龄
    public int getAge() {
        return age;
    }
    //set方法修改年龄
    public void setAge(int age) {
        if(age<0 ||age>100){
            return;
        }
        this.age = age;
    }

    //get方法获取姓名
    public String getName() {
        return name;
    }
    //set方法修改年龄
    public void setName(String name) {
        this.name = name;
    }
}

这样在操作的时候外部通过 get和set 修改和获取属性,这样的好处是我可以在get和set方法中隐藏一些其他的处理逻辑(比如在setAge里添加一些年龄的限制条件),且属性没有对外部暴露,也可以进一步提高安全性。如果没有进行封装,那任意调用属性会导致数据的错误、混乱或安全性问题(比如年龄:如果所以调用没有验证,那可能出现负数出现)。

另外我们对一些逻辑的封装可以极大的提高此段代码的复用性,比方一个求和的方法:

public static void main(String[] args) {
    //求和
    int sum = 0;
    for(int i=1; i<=10 ; i++){
        sum = sum+i;
    }
    System.out.println(sum);
	//封装成方法后可以随意调用,实现代码的复用,如果不封装,需要写多段求和代码才能实现
    System.out.println(getSum(10));
    System.out.println(getSum(100));
    System.out.println(getSum(20));
}
//把求和的逻辑封装成一个方法
public static int getSum(int num){
    int sum = 0;
    for(int i=1; i<=num ; i++){
        sum = sum+i;
    }
    return sum;
}



3.2、 继承

类和类之间有些也会具有一定的关系。比方说四边形,可以分为正方形、长方形、菱形,他们不但继承了四边形的特征,也具有属于自己的特征,这就是一种继承的关系。

image-20210619183234657

有时候我们希望基于某一个类进行扩展,使一个新类直接拥有基类的基本特征,而不需要重复去写,这就是继承的思想。比如说手机的逐步发展,最早的大哥大只有通话功能,后来的按键手机增加则界面等操作,再到现在的智能手机,它们不是一蹴而就的,而是通过在原有的功能上再增加新功能而逐渐演变过来的,就其实就是一种继承的直观体现。

继承原有的功能,增加自己新的功能,实现了拓展和复用。

在Java继承可以使用

extends 关键字

来实现,其中Java规定了java.lang.Object 类作为所有的类直接或间接的父类(当类没有继承其他类时,java默认继承Object类,当类继承了其他类时,可以向上追溯,最终继承的类就是Object类)。

java规定类只能继承一个类,但是一个类可以被多个类继承(一个子类只能有一个直接父类,一个父类可以有多个子类),类之间可以有多层的继承关系,直接继承的是直接父类,父类的父类就是间接父类,而最上层就是Object。

image-20230130172315045

Object类:

image-20230130174858815

Object类提供的方法:

image-20230130175337998

子类不会继承父类的构造方法,但是会调用(子类初始化前会先初始化父类)。如果父类的构造器带有参数,则必须在子类的构造器中显式地通过

super 关键字

调用父类的构造器并配以适当的参数列表(这就是Java要设置一个默认的无参构造的缘故,方便初始化)。

  1. 如果要初始化父类中的字段,可以在子类的构造方法中通过关键字super调用父类的构造方法;且该super语句必须在构造方法中的第一行。
  2. 如果父类构造器没有参数,则在子类的构造器中不需要使用 super 关键字调用父类构造器,系统会自动调用父类的无参构造器。
  3. 如果父类的构造器带有参数,则必须在子类的构造器中显式地通过 super 关键字调用父类的构造器并配以适当的参数列表;

image-20230130172944175


使用继承时,需要注意继承是受权限修饰符影响的。

  • 子类无法继承 private 修饰的属性和方法
  • 子类和父类在同一包下,可以继承 default 权限的属性方法
  • 子类可以对父类进行扩展,拥有自己的属性和方法
  • 子类可以重写父类的方法(前提是可以继承到这个方法)(多态的体现)

image-20230130173445713

虽然继承可以极大的提高代码的复用性,但是不能盲目的去继承,比如你让一个Dog类继承Person类,比如仅仅为了一个类中的某个功能,就直接使用继承。所以继承需要根据实际需要来选择是否使用。

继承中的关键字:extends、super 、this、final

image-20220412223402273

1、extends:单一继承,可以让一个类继承一个父类

2、super:我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。

3、this:指向自己的引用。引用自身的属性和方法。

4、final:当用final修饰类时,是把类定义为不能继承的,即最终类;

用于修饰方法时,该方法不能被子类重写:

用于修饰属性时,和static一起使用,表明为一个常量,各类的所有对象共用一个值



3.3、 多态

多态指同一个实体同时具有多种形式。同字面意思,即一个对象在不同的情况下会有不同的体现。(主要体现在对象和方法上,在子父类中不要定义同名的属性)

image-20210624173641337



3.3.1、对象的多态

类的多态其实就是一继承关系。



1、向上转型

向上转型其实就是

父类对子类的引用

。等边三角形是一种特殊的三角形,但是不管再怎么特殊它也是一个三角形。不管什么品种的狗我们都可以说它是一只动物。

这个特点其实就是设计原则中的里式替换原则的原理。子类至少是一个父类,所以父类出现的地方,其子类一定可以出现。

//狗继承与Animals ,所以可以向上转型,用Animals引用Dog类
//能引用是因为狗至少是一种动物,它有动物类所有属性和方法
Animals animals= new Dog();

向上转型的概念使用的地方很多,尤其是在框架学习阶段,会大量使用(在抽象类、接口等方面会大量的使用)。

子类中如果定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的(多态是在方法调用时,才会明确具体的方法)。

public class Test {
    public static void main(String[] args) {
        Random random = new Random();
        int choose = random.nextInt(3);
        System.out.println(choose);
        //编译期间是不会知道实例化那个对象的,需要在运行期间确定
        Animal animal = switch (choose) {
            case 1 -> new Animal();
            case 2 -> new Dog();
            default -> new Cat();
        };

        //而且传递的是哪个对象就调用那个对象的say方法
        animal.say();
    }
}
class Animal{
    public void say(){
        System.out.println("动物的叫声");
    }
}
class Dog extends Animal{
    @Override
    public void say() {
        System.out.println("汪汪汪!!!");
    }
}
class Cat extends Animal{
    @Override
    public void say() {
        System.out.println("喵喵喵!!!");
    }
}

如果使用了向上转型,声明为父类类型,虽然内存中实际加载的是子类对象,但是由于变量时父类的类型,会导致在编译时,只能使用父类声明的属性和方法,子类特有的属性和方法是不能调用的。所以父类还可以向下转型。



2、向下转型

向下转型是讲父类转型为子类,这种转型如果直接转化,通常会出现问题(如:ClassCastException异常),所以在具体使用向下转型的时候需要使用显式类型转换。(使用的较少)

向下转型的前提:

  1. 父类对象指向的是子类对象(实际上还是得先向上转型一下),如果指向的不是子类对象,是没法向下转型的。
  2. 有了1的前提,才能使用强制类型转换进行转型

向下转型通常配合

instanceof

关键字使用,用于判断一个实例对象是否属于某个类,判断一个类是否实现了某个接口。

a instanceof B  :判断对象a是否是类B的一个实例(或类a是否实现了接口B)

当我们使用向下转型时,可能会出现一些问题,所以在之前需要先判断一下。

class Animals {
    public void sound(){
        System.out.println("动物叫声");
    }
}
class Dog extends Animals{
    @Override
    public void sound() {
        System.out.println("狗叫声");
    }
    public void eat(){
        System.out.println("狗在吃骨头");
    }
}
class Cat extends Animals{
    @Override
    public void sound() {
        System.out.println("喵喵喵");
    }
    public void play(){
        System.out.println("猫在玩耍");
    }
}
class Test{
    public static void main(String[] args) {
        //向上转型
        Animals a = new Dog();
        // Animals a = new Cat();
        a.sound();
        //a.eat()方法时无法调用的,如果使用需要向下转型
        //向下转型,先判断属于Dog还是Cat的实例,属于谁的实例就转型成谁
        if(a instanceof Dog){
            Dog dog = (Dog) a;
            dog.eat();
        } else if (a instanceof Cat) {
            Cat cat = (Cat)a;
            cat.play();
        }
    }
}

image-20230131140758830

image-20230131142600442



3.3.2、方法的多态



1、重写

重写父类的方法,方法名字、参数、返回值相同

public class Persion {
	public void say(String name){
       System.out.println("名字是:"+name);
    }
}
public class Student extends Persion{
    public void say(String name) {		//重写了父类的方法,方法名和参数相同
        System.out.println(name+"是一个学生");
    }
}

image-20230130180159852



2、重载

同一个类中的相同名字不同参数的方法,调用时根据传递的参数不同来区分是哪个方法

public class Persion{
    public void say(String name , String sex){}
    public void say(String name,int age){}	//重载方法,名字相同,但是传递参数的类型必须有不同   

    //重载的参数类型不能相同
    public void say(String sex,String name){}	//和第一个say具有相同类型的参数,所以系统无法判定,就会出现错误     
} 

image-20230130180548564



3、重写和重载区别
1、重写(Override)
	重写是子类对父类允许访问的方法进行重写, 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类
	子类和父类在同一个包中,那么子类可以重写父类除了声明为 private 和 final 的方法外的所有方法
	子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
	访问权限不能比父类中被重写的方法的访问权限更低。
	父类的成员方法只能被它的子类重写,子类能够根据需要实现父类的方法
	重写方法抛出的异常范围不能大于父类。异常也有继承关系,所以子类能抛出的异常不能高于父类
	参数列表与被重写方法的参数列表必须完全相同。
	声明为 final 的方法不能被重写。声明为 static 的方法不能被重写,但是能够被再次声明。
	构造方法不能被重写。
	如果不能继承一个类,则不能重写该类的方法。
2、重载(Overload)
	重载是在一个类里面,方法名字必须相同,而参数必须不同。返回类型可以相同也可以不同。
	被重载的方法可以改变返回类型;
	被重载的方法可以改变访问修饰符;
	被重载的方法可以声明新的或更广的检查异常;
	无法以返回值类型作为重载函数的区分标准。
3、方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。
	方法重载是一个类中定义了多个同名的方法,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载
	方法重写是在子类存在方法与父类同名的方法,而且参数的个数与类型一样,就称为重写
	方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。



版权声明:本文为yuandfeng原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。