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

3、 面向对象三大特征


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

3.1、封装

封装是面向对象编程的核心思想之一,它将数据(属性)和操作数据的方法(行为)捆绑在一起,形成一个独立的单元类。同时,通过访问控制机制,隐藏内部的实现细节,只暴露必要的接口与外界交互。

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

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

在这里插入图片描述

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方法操作,可以防止属性对外暴露,同时在相关方法中添加相关逻辑(比如在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;
}

JDK 9 引入了 Java 平台模块系统(JPMS),为封装提供了更强大的支持。模块可以声明哪些包是导出的(公开的),哪些包是隐藏的(私有的)。这使得封装不仅可以在类级别实现,还可以在模块级别实现,进一步提高了代码的封装性和可维护性。

3.2、 继承

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

在这里插入图片描述

有时候我们更希望在某个功能的基础上进行扩展新功能,而不是推翻以前的功能重新设置,这就是继承的思想。比如说手机,从早期的大哥大,再到现在的智能手机,它们就是通过在原有的功能上再增加新功能而逐渐演变过来的,这就是一种继承的直观体现。继承原有的功能,增加自己新的功能,实现了拓展和复用。

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

  1. 基类(父类或超类):基类是在继承关系中处于上层的类。它提供了一些基本的属性和功能(一些类共有的一些代码),这些可以被继承到一个或多个派生类中。
  2. 派生类(子类):可以继承父类的方法和属性,但是使用时需要遵循相关权限,同时也可以添加自己的属性和方法,也可以重写(override)接收自基类的方法以实现不同的行为。

java规定类只能继承一个类,但是一个类可以被多个类继承(一个子类只能有一个直接父类,一个父类可以有多个子类),类之间可以有多层的继承关系,直接继承的是直接父类,父类的父类就是间接父类,而最上层就是Object。子类能从父类中获取成员变量、方法、内部类、内部接口、枚举(只能使用权限内允许的),不能获得构造器和初始化代码块。

  • 定义一个父类Person

    public class Person {
        //姓名
        String name;
        //定义为私有后子类就不能获取了
        private int age;
        //说话、睡觉
        public void say(){
            System.out.println("我的名字是:"+name);
        }
        public void sleep(){
            System.out.println("睡觉...");
        }
    }
    
  • 定义子类:Student,继承父类Person

    public class Student extends Person{
        //学号,所在班级
        int id;
        String classAndGrade;
        //学生需要进行学习
        public void study(){
            //Student类中是没有name属性的,但是这里可以使用,
            // 是因为继承的父类中存在该属性
            System.out.println(name+"正在学习中...");
        }
    	.....其他方法.....
    }
    
  • 定义测试类进行测试

    public static void main(String[] args) {
        Student s = new Student();
        //可以使用父类非私有的属性和方法
        s.name = "张三";
        s.id = 1001;
        s.classAndGrade = "高三一班";
        s.say();
        s.study();
        System.out.println(s.toString());
    }
    

在这里插入图片描述

Object类:在Java中,Object类是所有类的根类,所有对象(包括数组)都实现了这个类的方法。Object类位于 java.lang包下

在这里插入图片描述

Object类提供的方法:

在这里插入图片描述

子类不会继承父类的构造方法,但是会调用(初始化子类前会先初始化父类)。如果父类的构造器带有参数,则必须在子类的构造器中显式地通过 super 关键字调用父类的构造器并配以适当的参数列表(这就是Java为什么要设置一个默认的无参构造的缘故,方便初始化)。

在这里插入图片描述

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

虽然继承可以极大的提高代码的复用性,但是不能盲目的去继承,比如你让一个Dog类继承Person类,比如仅仅为了一个类中的某个功能,就直接使用继承。使用继承需要保证两个条件:

  1. 子类需要额外增加成员变量而非改变变量值
  2. 子类需要额外增加独有的行为方法

盲目的继承会破坏父类封装性,所以尽量把父类的成员变量设置为private,父类中的一些辅助方法也尽量设置为private,如果父类中需要外部访问,但却不想子类继承那么可以使用final修饰方法,如果父类希望方法被子类重写,但不需要被其他类访问,则可以设置为 protected。在父类的构造方法中尽量不要调用被子类重写的方法。

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

在这里插入图片描述

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

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

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

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

3.3、 多态

多态意味着一个对象可以有多种形式或表现方式。在 Java 中,多态允许您使用父类类型的引用来操作子类对象。这意味着,相同的操作可以应用于不同类型的对象,并根据对象的实际类型产生不同的行为。

在这里插入图片描述

3.3.1、类的转型

类的多态其实就是一继承关系。在Java中引用变量有两种类型:编译时类型、运行时类型。编译时类型由声明该变量时使用的类型决定,而运行时类型由实际赋给改变量的对象决定,当编译时类型和运行时类型不一致时就会出现多态。

1、向上转型

向上转型就是父类对子类的引用。等边三角形是一种特殊的三角形,但是不管再怎么特殊它也是一个三角形。不管什么品种的狗我们都可以说它是一只动物。

这个特点其实就是设计原则中的里式替换原则的原理。子类至少是一个父类,父类出现的地方,其子类一定可以出现。所以Java允许把子类对象直接赋值给父类引用变量。

//狗继承与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

instanceof在Java 17中又增加了模式匹配的功能,此时的instanceof 可以同时完成类型判断和类型转换。

public static void main(String[] args) {
	 Animals a = new Dog();
     //向下转型,先判断属于Dog还是Cat的实例,属于谁的实例就转型成谁
     //可以直接在判断后进行类型转换
     if(a instanceof Dog dog){
         dog.eat();
     } else if (a instanceof Cat cat) {
         cat.play();
     }
}
3.3.2、重载重写
1、重写

子类在继承父类后,可以重写父类中已经存在的方法(非类方法,且需要有相应权限的方法)。

  1. 不同的类中:重写方法必须在子类中定义,并且继承自父类的方法。
    • 子类和父类在同一个包中,那么子类可以重写父类除了声明为 private 和 final 的方法外的所有方法
    • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
  2. 方法名、参数列表相同:重写方法的名称、参数列表必须与被重写的相同。
  3. 返回类型相同或是其子类:重写方法的返回类型必须与被重写的方法返回类型相同,或者是其子类(协变返回类型:允许子类重写方法时返回更具体的类型,只要这个返回类型是父类方法返回类型的子类型即可)。
  4. 访问权限不能更严格:重写方法的访问权限不能比被重写的方法更严格
  5. 运行时多态性:重写方法在运行时根据对象的实际类型确定调用哪个方法。
class Animal {
    public void makeSound() {
        System.out.println("发出声音");
    }
}
class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("狗叫声");
    }
}
2、重载

Java中允许一个类中定义多个同名的方法,但是这些同名方法必须有不同的参数,调用时根据传递的参数不同来区分是哪个方法:

  • 同一个类中,名称相同:重载的方法需要在同一个类中,且名字相同
  • 参数列表不同:可以是参数数量不同、参数类型不同、参数顺序不同(具体分析)。
  • 返回类型、访问修饰符可以不同:方法的返回类型或访问修饰符不参与区分重载,它们可以相同也可以不同。
public class Persion{
    public void say(String name , String sex){}
    public void say(String name,int age){}	//重载方法,名字相同,但是传递参数的类型必须有不同   

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

方法重载通常用于创建功能相似但接受不同类型或数量参数的方法,使得方法调用更加灵活,增强了程序的可读性和易用性。(例如,标准库中的println方法就通过重载提供了多种打印不同类型数据的能力。)

3、重写和重载对比
特性 重载(Overloading) 重写(Overriding)
定义 在同一个类中的同名不同参数的方法 子类重写父类中的方法(授权限修饰符的控制)
类关系 发生在同一个类中 发生在子类与父类之间
参数列表 必须不同 必须相同
返回类型 可以相同或不同 必须相同或是其子类
访问修饰符 可以相同或不同 子类方法的访问修饰符不能更严格
异常 可以相同或不同 子类方法抛出的异常不能比父类方法更多
多态类型 编译时多态 运行时多态
绑定方式 静态绑定 动态绑定
调用方式 在编译时,根据参数类型和数量确定调用哪个方法。 在运行时,根据对象的实际类型确定调用哪个方法。
时机 编译时决定调用哪个方法 运行时根据对象的实际类型决定调用哪个方法
4、覆盖和遮蔽

在Java中,当父类和子类有同名的属性和方法时,会出现方法覆盖(Override)和属性遮蔽(Shadowing)的情况。

  • 方法覆盖(Override):当子类有一个与父类完全相同的方法时,我子类的方法会覆盖了父类的方法。这意味着当通过子类对象调用该方法时,将执行子类中的版本(基于对象的实际类型)
    • 动态绑定:方法调用在运行时解析,JVM根据对象实际类型来确定调用哪个方法(等号右边new的对象)
  • 属性遮蔽(Shadowing):当子类声明一个与父类同名的属性时,子类的属性会遮蔽(或隐藏)父类的属性。这意味着通过父类的引用访问该属性时,将访问父类中定义的属性。
    • 静态绑定:属性的访问在编译时就已确定,根据引用类型来确定访问哪个属性(等号左边的引用类型)
class Parent {
    String name = "父类属性";
    void show() {
        System.out.println("父类方法");
    }
}
//子类
class Child extends Parent {
    String name = "子类属性";
    @Override
    void show() {
        System.out.println("子类方法");
    }
}

通过父类引用来操作这两个类的对象:

//父类引用子类对象
Parent obj = new Child();
// 访问属性
System.out.println(obj.name); 
// 调用方法
obj.show();  

image-20240415135615339

在Java中,通过父类引用子类对象时,方法调用遵循动态绑定(多态),会调用实际对象(=右侧)的方法;而属性访问遵循静态绑定,访问的是引用类型(=左侧)的属性。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐